1use crate::*;
9use alux_ext::ext;
10use anyhow::{Result, bail};
11use rsynko_download::*;
12use std::fmt::{Debug, Display};
13use std::path::{Path, PathBuf};
14
15pub trait ExtractionLawFixture {
17 fn accepted_law_url(&self) -> String;
19 fn unsupported_law_url(&self) -> String;
21 fn law_applied_extractors(&self) -> Vec<String>;
23}
24
25#[ext(name = ObservationLaws)]
27pub impl<This> This
28where
29 This: MetadataAlg,
30{
31 fn observation_laws(&self) -> Result<()>
37 where
38 This::Metadata: PartialEq + Debug,
39 This::Value: Clone,
40 {
41 if self.metadata([]) != self.empty_metadata() {
42 bail!("neutrality violated: a record of no fields differs from the empty record");
43 }
44 let stated = self.string_metadata("first");
45 if self.metadata([("title".to_owned(), stated.clone())]) != self.text_metadata("title", "first") {
46 bail!("single-field construction disagrees with its derived form");
47 }
48 let replaced =
49 self.metadata([("title".to_owned(), stated), ("title".to_owned(), self.string_metadata("second"))]);
50 if replaced != self.text_metadata("title", "second") {
51 bail!("replacement violated: a later equal key did not replace the earlier field");
52 }
53 Ok(())
54 }
55}
56
57#[ext(name = SelectionLaws)]
59pub impl<This> This
60where
61 This: MetadataAlg
62 + FormatAlg
63 + FormatPredicateAlg
64 + FormatSelectionAlg
65 + FormatPredicateMatchAlg
66 + FormatSelectionApplyAlg,
67{
68 fn selection_laws(&self) -> Result<()>
74 where
75 This::Predicate: Clone,
76 {
77 let catalog = [false, true, true].map(|video| {
78 self.format(
79 if video { "video" } else { "audio" },
80 self.metadata([(FORMAT_HAS_VIDEO.to_owned(), self.boolean_metadata(video))]),
81 )
82 });
83 self.check_selection_against(&catalog, &self.has_video())?;
84 self.check_selection_against(&catalog, &self.format_extension("never"))?;
85
86 let sized = [360_i64, 720, 1080]
88 .map(|height| self.format("sized", self.metadata([("height".to_owned(), self.integer_metadata(height))])));
89 self.check_selection_against(&sized, &self.at_most("height", 720.0))?;
90 self.check_selection_against(&sized, &self.greater_than("height", 720.0))?;
91 self.check_selection_against(&catalog, &self.at_most("height", 720.0))?;
93 self.check_selection_against(&catalog, &self.observed_format("height"))
94 }
95
96 fn check_selection_against(&self, catalog: &[This::Format], predicate: &This::Predicate) -> Result<()>
102 where
103 This::Predicate: Clone,
104 {
105 let matching: Vec<&This::Format> =
106 catalog.iter().filter(|format| self.format_matches(predicate, format)).collect();
107 let best = self.select_formats(catalog, &self.best_format(predicate.clone()));
108 let worst = self.select_formats(catalog, &self.worst_format(predicate.clone()));
109
110 let (Some(first), Some(last)) = (matching.first(), matching.last()) else {
111 if best.is_some() || worst.is_some() {
112 bail!("selection chose a format although nothing matches the predicate");
113 }
114 return Ok(());
115 };
116 if !chose(best.as_deref(), last) {
117 bail!("Best did not choose the most preferred matching format");
118 }
119 if !chose(worst.as_deref(), first) {
120 bail!("Worst did not choose the least preferred matching format");
121 }
122
123 let merged = self.select_formats(
124 catalog,
125 &self.merge_formats([self.best_format(predicate.clone()), self.worst_format(predicate.clone())]),
126 );
127 let Some(merged) = merged else {
128 bail!("Merge failed although every child succeeded");
129 };
130 if merged.len() != 2 || !std::ptr::eq(merged[0], *last) || !std::ptr::eq(merged[1], *first) {
131 bail!("Merge did not preserve child-program order");
132 }
133
134 let fallback = self.select_formats(
135 catalog,
136 &self.fallback_formats([self.best_format(predicate.clone()), self.worst_format(predicate.clone())]),
137 );
138 if !chose(fallback.as_deref(), last) {
139 bail!("Fallback was not left-biased");
140 }
141 Ok(())
142 }
143}
144
145fn chose<Format>(selected: Option<&[&Format]>, expected: &Format) -> bool {
147 selected.is_some_and(|chosen| chosen.len() == 1 && std::ptr::eq(chosen[0], expected))
148}
149
150#[ext(name = FormatLaws)]
152pub impl<This> This
153where
154 This: MetadataAlg + FormatAlg + FormatViewAlg,
155{
156 fn format_laws(&self) -> Result<()> {
168 let source = "https://media.example/a.mp4";
169 let stated = self.format(
170 "18",
171 self.metadata([
172 (FORMAT_SOURCE.to_owned(), self.string_metadata(source)),
173 (FORMAT_EXTENSION.to_owned(), self.string_metadata("mp4")),
174 ]),
175 );
176 if self.format_text(&stated, FORMAT_SOURCE) != Some(source) {
177 bail!("a stated source is not the one the format is observed under");
178 }
179 if self.format_text(&stated, FORMAT_EXTENSION) != Some("mp4") {
180 bail!("stating a source disturbed the stated extension");
181 }
182 if let Some(observed) = self.format_text(&stated, "quality") {
183 bail!("an unstated observation was observed as {observed}");
184 }
185 let bare = self.format("18", self.empty_metadata());
186 if self.format_text(&bare, FORMAT_SOURCE).is_some() {
187 bail!("a format stating nothing still locates its bytes");
188 }
189 Ok(())
190 }
191}
192
193#[ext(name = ArtifactLaws)]
195pub impl<This> This
196where
197 This: MetadataAlg + ArtifactAlg,
198{
199 fn artifact_laws(&self) -> Result<()>
210 where
211 This::Artifact: PartialEq + Debug,
212 This::Metadata: Clone,
213 {
214 let resting = self.metadata([(ARTIFACT_LOCATION.to_owned(), self.string_metadata("temporary.part"))]);
215 let stated = self.artifact("media", ArtifactKind::Media, resting.clone());
216 if stated != self.artifact("media", ArtifactKind::Media, resting.clone()) {
217 bail!("definition is not a function of identity, role, and observations");
218 }
219 for (name, other) in [
220 ("identity", self.artifact("subtitle", ArtifactKind::Media, resting.clone())),
221 ("role", self.artifact("media", ArtifactKind::Subtitle, resting)),
222 ("observations", self.artifact("media", ArtifactKind::Media, self.empty_metadata())),
223 ] {
224 if stated == other {
225 bail!("artifact {name} is not significant: {stated:?} equals {other:?}");
226 }
227 }
228 Ok(())
229 }
230}
231
232pub trait ProcessingLawFixture {
234 fn law_applied(&self) -> Vec<String>;
236 fn refuse_law_processor(&mut self, id: &str);
238}
239
240#[ext(name = ProcessingApplicationLaws)]
242pub impl<This> This
243where
244 This: ProcessingProgramAlg + ProcessingProgramViewAlg + ProcessingApplyAlg + ProcessingLawFixture,
245{
246 fn processing_stage_laws(&mut self) -> Result<()>
252 where
253 This::Error: Debug,
254 {
255 let program = self.processing([
256 self.processing_step(ProcessingStage::PreProcess, self.processor("before")),
257 self.processing_step(ProcessingStage::AfterMove, self.processor("first")),
258 self.processing_step(ProcessingStage::AfterMove, self.processor("second")),
259 ]);
260 if let Err(error) = self.run_processing_stage(&program, ProcessingStage::AfterMove) {
261 bail!("stage interpretation failed: {error:?}");
262 }
263 let applied = self.law_applied();
264 if applied != ["first", "second"] {
265 bail!("stage selection did not preserve relative declaration order: {applied:?}");
266 }
267 Ok(())
268 }
269
270 fn processing_failure_laws(&mut self) -> Result<()> {
276 let program = self.processing([
277 self.processing_step(ProcessingStage::PostProcess, self.processor("first")),
278 self.processing_step(ProcessingStage::PostProcess, self.processor("refused")),
279 self.processing_step(ProcessingStage::PostProcess, self.processor("never")),
280 ]);
281 self.refuse_law_processor("refused");
282 if self.run_processing_program(&program).is_ok() {
283 bail!("a refused step did not fail the program");
284 }
285 let applied = self.law_applied();
286 if applied != ["first"] {
287 bail!("failure did not stop interpretation before later steps: {applied:?}");
288 }
289 Ok(())
290 }
291}
292
293#[ext(name = ProcessingLaws)]
295pub impl<This> This
296where
297 This: ProcessingProgramAlg + ProcessingProgramViewAlg,
298{
299 fn processing_laws(&self) -> Result<()>
308 where
309 This::Step: Clone + PartialEq + Debug,
310 {
311 let authored: Vec<This::Step> =
312 [(ProcessingStage::PostProcess, "first"), (ProcessingStage::AfterMove, "second")]
313 .into_iter()
314 .map(|(stage, name)| self.processing_step(stage, self.processor(name)))
315 .collect();
316
317 if Self::processing_steps(&self.processing([])).next().is_some() {
318 bail!("the empty program is not empty");
319 }
320 let program = self.processing(authored.iter().cloned());
321 let declared: Vec<This::Step> = Self::processing_steps(&program).cloned().collect();
322 if declared != authored {
323 bail!("construction did not preserve declaration order: {declared:?}");
324 }
325 for (side, composed) in [
326 ("left", self.then_processing(self.processing([]), self.processing(authored.iter().cloned()))),
327 ("right", self.then_processing(self.processing(authored.iter().cloned()), self.processing([]))),
328 ] {
329 let observed: Vec<This::Step> = Self::processing_steps(&composed).cloned().collect();
330 if observed != authored {
331 bail!("the empty program is not an identity on the {side}: {observed:?}");
332 }
333 }
334 let doubled =
335 self.then_processing(self.processing(authored.iter().cloned()), self.processing(authored.iter().cloned()));
336 let observed: Vec<This::Step> = Self::processing_steps(&doubled).cloned().collect();
337 let expected: Vec<This::Step> = authored.iter().chain(&authored).cloned().collect();
338 if observed != expected {
339 bail!("concatenation is not sequential composition: {observed:?}");
340 }
341 Ok(())
342 }
343}
344
345#[ext(name = OutputNameLaws)]
347pub impl<This> This
348where
349 This: OutputNameAlg,
350{
351 fn output_naming_laws(&self) -> Result<()> {
357 let named = portable_file_name(Some("A/B"), "id", Some("mp4"));
358 if named != portable_file_name(Some("A/B"), "id", Some("mp4")) {
359 bail!("naming is not a function of title, fallback, and extension");
360 }
361 if portable_file_name(None, "id", Some("mp4")) != portable_file_name(Some(" "), "id", Some("mp4")) {
362 bail!("a blank title did not select the stated fallback identity");
363 }
364 for candidate in [&named, &portable_user_file_name("a/b.mp4", Some("mp4"))] {
365 if candidate.components().count() != 1 {
366 bail!("naming produced a path rather than one file component: {}", candidate.display());
367 }
368 }
369 if portable_user_file_name("edited.mp4", Some("bin")).extension().and_then(|value| value.to_str())
370 != Some("mp4")
371 {
372 bail!("an edited name did not keep its valid explicit extension");
373 }
374 Ok(())
375 }
376}
377
378#[ext(name = ExtractionLaws)]
380pub impl<This> This
381where
382 This: ExtractionCatalogAlg + ExtractionApplyAlg + ExtractionLawFixture,
383{
384 fn extraction_laws(&self) -> Result<()>
390 where
391 This::Extractor: PartialEq + Debug,
392 This::Extraction: Debug,
393 This::Error: Debug,
394 {
395 let accepted = self.accepted_law_url();
396 let unsupported = self.unsupported_law_url();
397 let expected = self.extractor_keys().find(|key| self.extractor_accepts(key, &accepted));
398 let Some(expected) = expected else {
399 bail!("the fixture URL {accepted} is accepted by no catalog entry");
400 };
401 match self.choose_extractor(&accepted) {
402 Some(chosen) if chosen == expected => {}
403 other => bail!("first-accepting choice violated: chose {other:?}"),
404 }
405 if let Some(chosen) = self.choose_extractor(&unsupported) {
406 bail!("unsupported input chose {chosen:?}, expected no extractor");
407 }
408 if let Err(error) = self.extract_url(&accepted) {
409 bail!("accepted input failed to extract: {error:?}");
410 }
411 let applied = self.law_applied_extractors();
412 if applied.len() != 1 {
413 bail!("extracting a supported URL applied {applied:?}, expected exactly one extractor");
414 }
415 match self.extract_url(&unsupported) {
416 Err(ExtractUrlError::Unsupported { url }) if url == unsupported => {}
417 other => bail!("unsupported input denoted {other:?}, expected an unsupported error"),
418 }
419 if self.law_applied_extractors() != applied {
420 bail!("extracting an unsupported URL applied an extractor");
421 }
422 Ok(())
423 }
424}
425
426#[ext(name = MediaProgramLaws)]
428pub impl<This, Source, Event, Progress> This
429where
430 This: MetadataAlg
431 + FormatAlg
432 + FormatViewAlg
433 + ExtractionAlg
434 + ExtractionViewAlg
435 + MediaViewAlg
436 + FormatPredicateAlg
437 + FormatSelectionAlg
438 + FormatSelectionApplyAlg
439 + FormatSourceAlg<Source>
440 + FetchStreamAlg<Source, Error: Display>
441 + AtomicPublishAlg<Error: Display>
442 + DownloadObservationAlg<Event = Event, Progress = Progress>
443 + DownloadProgressAlg<Progress = Progress>
444 + DownloadReportAlg<Event = Event>
445 + MediaProgramLawFixture,
446{
447 fn media_program_laws(&mut self) -> Result<()> {
460 let format = self.format(
461 "18",
462 self.metadata([
463 (FORMAT_SOURCE.to_owned(), self.string_metadata(self.located_law_source())),
464 (FORMAT_EXTENSION.to_owned(), self.string_metadata(self.law_extension())),
465 ]),
466 );
467 let extraction = self.media("law-media", self.empty_metadata(), [format]);
468 let selection = self.best_format(self.any_format());
469
470 let published = match self.download_extraction(extraction, &selection, &OutputTarget::MediaId) {
471 Ok(path) => path,
472 Err(error) => bail!("the composed program failed: {error}"),
473 };
474 let expected = PathBuf::from(format!("law-media.{}", self.law_extension()));
475 if published != expected {
476 bail!("naming derived {} rather than {}", published.display(), expected.display());
477 }
478 if self.law_published_bytes(&published).as_deref() != Some(&self.expected_law_bytes()[..]) {
479 bail!("the selected format's source did not reach download unchanged");
480 }
481
482 let reference = self.url_reference("https://example.test/other", None, false);
483 match self.download_extraction(reference, &selection, &OutputTarget::MediaId) {
484 Err(MediaDownloadError::NotSingleMedia) => Ok(()),
485 other => bail!("a non-media result was not refused: {}", describe(&other)),
486 }
487 }
488}
489
490pub trait MediaProgramLawFixture {
492 fn located_law_source(&self) -> String;
494 fn law_extension(&self) -> String;
496 fn expected_law_bytes(&self) -> Vec<u8>;
498 fn law_published_bytes(&self, path: &Path) -> Option<Vec<u8>>;
500}
501
502fn describe<Ok, Error: Display>(outcome: &Result<Ok, Error>) -> String {
504 match outcome {
505 Ok(_) => "a successful download".to_owned(),
506 Err(error) => error.to_string(),
507 }
508}