Skip to main content

kcode_k1_web_bootstrap_import/
lib.rs

1#![doc = include_str!("../Documentation.md")]
2#![forbid(unsafe_code)]
3
4use kcode_k1_transaction_id::TxId;
5use kcode_k1_web_bootstrap_archive::{Archive, Package};
6use kcode_k1_web_bootstrap_preflight::inspect;
7use kcode_k1_web_coding::{CheckOutcome, K1WebCoding, WebCodingConfig, WebCodingRevisions};
8use kcode_k1_web_package::{
9    AuthorityId, DependencySelector, SourceFile, SourcePackage, WebFamily, WebId,
10};
11use kcode_k1_web_podman::WebPodmanConfig;
12use kcode_k1_web_projection::{K1WebProjection, PublishStatus};
13use semver::Version;
14use serde::Serialize;
15use sha2::{Digest, Sha256};
16use std::collections::BTreeSet;
17use std::fs::{File, OpenOptions};
18use std::io::Write;
19use std::path::{Path, PathBuf};
20use std::sync::Arc;
21
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct ImporterRevisions {
24    pub boot: String,
25    pub schema: String,
26    pub route: String,
27    pub harness: String,
28    pub check_policy: String,
29}
30
31#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct ImportedPackage {
33    name: String,
34    version: Version,
35    source_sha256: [u8; 32],
36}
37
38impl ImportedPackage {
39    pub fn name(&self) -> &str {
40        &self.name
41    }
42
43    pub fn version(&self) -> &Version {
44        &self.version
45    }
46
47    pub const fn source_sha256(&self) -> [u8; 32] {
48        self.source_sha256
49    }
50}
51
52#[derive(Clone)]
53pub struct WebBootstrapImporter {
54    inner: Arc<Inner>,
55}
56
57struct Inner {
58    cache_root: PathBuf,
59    projection_root: PathBuf,
60    revisions: ImporterRevisions,
61    podman: WebPodmanConfig,
62    projection: Arc<K1WebProjection>,
63}
64
65impl WebBootstrapImporter {
66    pub fn new(
67        cache_root: impl Into<PathBuf>,
68        projection_root: impl Into<PathBuf>,
69        revisions: ImporterRevisions,
70        podman: WebPodmanConfig,
71        projection: Arc<K1WebProjection>,
72    ) -> Result<Self, String> {
73        let cache_root = cache_root.into();
74        let projection_root = projection_root.into();
75        require_directory(&cache_root, "Web bootstrap cache")?;
76        require_directory(&projection_root, "Web projection")?;
77        if [
78            &revisions.boot,
79            &revisions.schema,
80            &revisions.route,
81            &revisions.harness,
82            &revisions.check_policy,
83        ]
84        .iter()
85        .any(|value| value.is_empty())
86        {
87            return Err("Web bootstrap importer revisions must be nonempty".to_owned());
88        }
89        Ok(Self {
90            inner: Arc::new(Inner {
91                cache_root,
92                projection_root,
93                revisions,
94                podman,
95                projection,
96            }),
97        })
98    }
99
100    pub fn import_all(
101        &self,
102        archive: &Archive,
103        authority: [u8; 12],
104        user: [u8; 12],
105        log_path: impl AsRef<Path>,
106    ) -> Result<Vec<ImportedPackage>, String> {
107        let log_path = log_path.as_ref();
108        let mut log = ImportLog::append(log_path)?;
109        let plan = inspect(archive);
110        log.line(&format!(
111            "IMPORT START packages={} warnings={}",
112            plan.packages().len(),
113            plan.warnings().len()
114        ))?;
115
116        let authority = AuthorityId::new(TxId::from_bytes(authority));
117        let mut prepared = std::iter::repeat_with(|| None)
118            .take(archive.packages().len())
119            .collect::<Vec<Option<PreparedPackage>>>();
120        let mut failures = 0_usize;
121
122        for &index in plan.order() {
123            let package = &archive.packages()[index];
124            let label = package_label(package);
125            match prepare_package(package, authority) {
126                Ok(item) => prepared[index] = Some(item),
127                Err(error) => {
128                    log.line(&format!("ERROR prepare {label}: {error}"))?;
129                    failures += 1;
130                }
131            }
132        }
133
134        let mut coding = self.open_diagnostic_coding(user, &mut log)?;
135        let authorize = |family: &WebFamily| Ok(family.authority() == authority);
136        let mut imported = Vec::with_capacity(archive.packages().len() - failures);
137
138        for &index in plan.order() {
139            let Some(item) = prepared[index].as_ref() else {
140                continue;
141            };
142            let label = coordinate_label(&item.coordinate);
143            log.line(&format!("ATTEMPT {label}"))?;
144
145            match coding.as_mut() {
146                Some(coding) => {
147                    log_chromium_check(coding, item, &authorize, &label, &mut log)?;
148                }
149                None => {
150                    log.line(&format!(
151                        "WARN CHECK {label}: Chromium diagnostics unavailable; proceeding with trusted immutable publication"
152                    ))?;
153                }
154            }
155
156            match self.publish_one(item) {
157                Ok(()) => {
158                    log.line(&format!("IMPORTED {label}"))?;
159                    imported.push(ImportedPackage {
160                        name: item.coordinate.name.clone(),
161                        version: item.coordinate.version.clone(),
162                        source_sha256: item.source_sha256,
163                    });
164                }
165                Err(error) => {
166                    log.line(&format!("ERROR import {label}: {error}"))?;
167                    failures += 1;
168                }
169            }
170        }
171
172        if failures != 0 {
173            log.line(&format!(
174                "FAILURE imported={} remaining={failures}",
175                imported.len()
176            ))?;
177            return Err(format!(
178                "Web bootstrap left {failures} package(s) concretely unimported; inspect {}",
179                log_path.display()
180            ));
181        }
182
183        log.line(&format!("SUCCESS packages={}", imported.len()))?;
184        Ok(imported)
185    }
186
187    fn open_diagnostic_coding(
188        &self,
189        user: [u8; 12],
190        log: &mut ImportLog,
191    ) -> Result<Option<K1WebCoding>, String> {
192        let revisions = &self.inner.revisions;
193        let config = match WebCodingConfig::new(
194            WebCodingRevisions {
195                boot: revisions.boot.clone(),
196                schema: revisions.schema.clone(),
197                route: revisions.route.clone(),
198                harness: revisions.harness.clone(),
199                check_policy: revisions.check_policy.clone(),
200            },
201            self.inner.projection_root.clone(),
202            self.inner.podman.clone(),
203        ) {
204            Ok(config) => config,
205            Err(error) => {
206                log.line(&format!(
207                    "WARN Chromium diagnostic configuration unavailable: {error:#?}"
208                ))?;
209                return Ok(None);
210            }
211        };
212
213        match K1WebCoding::open(
214            &self.inner.cache_root,
215            TxId::from_bytes(user),
216            config,
217            Arc::clone(&self.inner.projection),
218        ) {
219            Ok(coding) => Ok(Some(coding)),
220            Err(error) => {
221                log.line(&format!(
222                    "WARN Chromium diagnostic runner unavailable: {error:#?}"
223                ))?;
224                Ok(None)
225            }
226        }
227    }
228
229    fn publish_one(&self, item: &PreparedPackage) -> Result<(), String> {
230        let label = coordinate_label(&item.coordinate);
231        match self
232            .inner
233            .projection
234            .load(item.source.id())
235            .map_err(|error| format!("inspect existing publication: {error}"))?
236        {
237            Some(existing) if existing.as_ref() == &item.source => return Ok(()),
238            Some(_) => {
239                return Err(format!(
240                    "published Web package {label} has conflicting source bytes"
241                ));
242            }
243            None => {}
244        }
245
246        match self.inner.projection.publish(&item.source) {
247            Ok(outcome) if outcome.status != PublishStatus::Conflict => Ok(()),
248            Ok(_) => Err(format!(
249                "published Web package {label} reported an immutable conflict"
250            )),
251            Err(error) => match self.inner.projection.load(item.source.id()) {
252                Ok(Some(existing)) if existing.as_ref() == &item.source => Ok(()),
253                Ok(Some(_)) => Err(format!(
254                    "publish projection reported {error:?}; canonical Web package {label} has conflicting source bytes"
255                )),
256                Ok(None) => Err(format!(
257                    "publish projection reported {error:?}; no canonical publication exists"
258                )),
259                Err(reconcile) => Err(format!(
260                    "publish projection reported {error:?}; inspect canonical publication: {reconcile}"
261                )),
262            },
263        }
264    }
265}
266
267fn log_chromium_check(
268    coding: &mut K1WebCoding,
269    item: &PreparedPackage,
270    authorize: &impl Fn(&WebFamily) -> Result<bool, String>,
271    label: &str,
272    log: &mut ImportLog,
273) -> Result<(), String> {
274    if let Err(error) = coding.write(&item.source, authorize) {
275        return log.line(&format!(
276            "WARN CHECK {label}: diagnostic staging unavailable: {error:#?}; proceeding with trusted immutable publication"
277        ));
278    }
279
280    match coding.check_fresh(item.source.id(), authorize) {
281        Ok(CheckOutcome::Checked(execution)) => {
282            log.line(&format!("CHECK {label}: {execution:#?}"))
283        }
284        Ok(CheckOutcome::Reused) => log.line(&format!(
285            "WARN CHECK {label}: fresh Chromium check was unexpectedly reused; proceeding with trusted immutable publication"
286        )),
287        Err(error) => log.line(&format!(
288            "WARN CHECK {label}: {error:#?}; proceeding with trusted immutable publication"
289        )),
290    }
291}
292
293pub fn write_preflight_log(archive: &Archive, log_path: impl AsRef<Path>) -> Result<(), String> {
294    let log_path = log_path.as_ref();
295    let plan = inspect(archive);
296    let mut log = ImportLog::replace(log_path)?;
297    log.line(&format!(
298        "START Web bootstrap preflight packages={}",
299        plan.packages().len()
300    ))?;
301    for warning in plan.warnings() {
302        log.line(warning)?;
303    }
304    log.line(&format!(
305        "PREFLIGHT COMPLETE packages={} warnings={}",
306        plan.packages().len(),
307        plan.warnings().len()
308    ))
309}
310
311struct ImportLog(File);
312
313impl ImportLog {
314    fn replace(path: &Path) -> Result<Self, String> {
315        OpenOptions::new()
316            .create(true)
317            .truncate(true)
318            .write(true)
319            .open(path)
320            .map(Self)
321            .map_err(|error| {
322                format!(
323                    "initialize Web bootstrap import log {}: {error}",
324                    path.display()
325                )
326            })
327    }
328
329    fn append(path: &Path) -> Result<Self, String> {
330        OpenOptions::new()
331            .create(true)
332            .append(true)
333            .open(path)
334            .map(Self)
335            .map_err(|error| format!("open Web bootstrap import log {}: {error}", path.display()))
336    }
337
338    fn line(&mut self, line: &str) -> Result<(), String> {
339        writeln!(self.0, "{line}")
340            .map_err(|error| format!("write Web bootstrap import log: {error}"))?;
341        self.0
342            .flush()
343            .map_err(|error| format!("flush Web bootstrap import log: {error}"))
344    }
345}
346
347#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
348struct Coordinate {
349    name: String,
350    version: Version,
351}
352
353#[derive(Debug)]
354struct PreparedPackage {
355    coordinate: Coordinate,
356    source_sha256: [u8; 32],
357    source: SourcePackage,
358}
359
360#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
361struct GeneratedDependency {
362    authority: String,
363    name: String,
364    selector: String,
365}
366
367#[derive(Serialize)]
368struct Header<'a> {
369    dependencies: &'a [GeneratedDependency],
370}
371
372#[derive(Serialize)]
373struct Manifest<'a> {
374    name: &'a str,
375    version: String,
376    entry: &'a str,
377    tests: &'a str,
378    dependencies: &'a [GeneratedDependency],
379}
380
381fn prepare_package(package: &Package, authority: AuthorityId) -> Result<PreparedPackage, String> {
382    let coordinate = Coordinate {
383        name: package.name().to_owned(),
384        version: package.version().clone(),
385    };
386    let family = WebFamily::new(authority, coordinate.name.clone()).map_err(|error| {
387        format!(
388            "assign Web package {} authority: {error}",
389            coordinate_label(&coordinate)
390        )
391    })?;
392    let id = WebId::new(family, coordinate.version.clone()).map_err(|error| {
393        format!(
394            "assign Web package {} version: {error}",
395            coordinate_label(&coordinate)
396        )
397    })?;
398    let authority_text = authority.to_string();
399    let (dependencies, rewrites) = usable_dependencies(package, authority, &coordinate);
400
401    let original_documentation = package
402        .files()
403        .iter()
404        .find(|file| file.path() == "Documentation.md")
405        .ok_or_else(|| {
406            format!(
407                "Web bootstrap package {} lacks Documentation.md",
408                coordinate_label(&coordinate)
409            )
410        })?
411        .bytes();
412
413    let header = serde_json::to_string(&Header {
414        dependencies: &dependencies,
415    })
416    .map_err(|error| {
417        format!(
418            "encode Web dependencies for {}: {error}",
419            coordinate_label(&coordinate)
420        )
421    })?;
422    let mut documentation = format!("<!-- k1-web/v1\n{header}\n-->\n").into_bytes();
423    documentation.extend_from_slice(original_documentation);
424
425    let manifest = serde_json::to_vec(&Manifest {
426        name: package.name(),
427        version: package.version().to_string(),
428        entry: package.entry(),
429        tests: package.tests(),
430        dependencies: &dependencies,
431    })
432    .map_err(|error| {
433        format!(
434            "encode Web manifest for {}: {error}",
435            coordinate_label(&coordinate)
436        )
437    })?;
438
439    let mut files = Vec::with_capacity(package.files().len() + 1);
440    for file in package.files() {
441        if matches!(file.path(), "Documentation.md" | "k1-web.json") {
442            continue;
443        }
444        files.push(SourceFile::new(
445            file.path(),
446            rewrite_file(file.path(), file.bytes(), &authority_text, &rewrites),
447        ));
448    }
449    files.push(SourceFile::new("Documentation.md", documentation));
450    files.push(SourceFile::new("k1-web.json", manifest));
451
452    let source = SourcePackage::new(id, files).map_err(|error| {
453        format!(
454            "construct Web bootstrap package {}: {error}",
455            coordinate_label(&coordinate)
456        )
457    })?;
458    Ok(PreparedPackage {
459        coordinate,
460        source_sha256: source_digest(&source),
461        source,
462    })
463}
464
465fn usable_dependencies(
466    package: &Package,
467    authority: AuthorityId,
468    coordinate: &Coordinate,
469) -> (Vec<GeneratedDependency>, BTreeSet<(String, String)>) {
470    let authority_text = authority.to_string();
471    let mut dependencies = BTreeSet::new();
472    let mut rewrites = BTreeSet::new();
473
474    for dependency in package.dependencies() {
475        if WebFamily::new(authority, dependency.name().to_owned()).is_err() {
476            continue;
477        }
478        let Ok(selector) = DependencySelector::parse(dependency.selector()) else {
479            continue;
480        };
481        rewrites.insert((
482            dependency.name().to_owned(),
483            dependency.selector().to_owned(),
484        ));
485        if dependency.name() == coordinate.name && selector.matches(&coordinate.version) {
486            continue;
487        }
488        dependencies.insert(GeneratedDependency {
489            authority: authority_text.clone(),
490            name: dependency.name().to_owned(),
491            selector: dependency.selector().to_owned(),
492        });
493    }
494
495    (dependencies.into_iter().collect(), rewrites)
496}
497
498fn rewrite_file(
499    path: &str,
500    bytes: &[u8],
501    authority: &str,
502    declarations: &BTreeSet<(String, String)>,
503) -> Vec<u8> {
504    if !matches!(extension(path), "js" | "mjs" | "html" | "css") {
505        return bytes.to_vec();
506    }
507    let Ok(source) = std::str::from_utf8(bytes) else {
508        return bytes.to_vec();
509    };
510
511    let mut rewritten = source.to_owned();
512    for (name, selector) in declarations {
513        let legacy = format!("/module/{name}/v{selector}/");
514        let route_selector = if selector == "*" {
515            "*".to_owned()
516        } else {
517            format!("v{selector}")
518        };
519        let current = format!("/{authority}/{name}/{route_selector}/");
520        rewritten = rewritten.replace(&legacy, &current);
521    }
522    rewritten.into_bytes()
523}
524
525fn extension(path: &str) -> &str {
526    path.rsplit_once('.').map_or("", |(_, extension)| extension)
527}
528
529fn source_digest(source: &SourcePackage) -> [u8; 32] {
530    let mut hash = Sha256::new();
531    for file in source.files() {
532        hash.update((file.path().len() as u64).to_le_bytes());
533        hash.update(file.path().as_bytes());
534        hash.update((file.bytes().len() as u64).to_le_bytes());
535        hash.update(file.bytes());
536    }
537    hash.finalize().into()
538}
539
540fn package_label(package: &Package) -> String {
541    format!("{} {}", package.name(), package.version())
542}
543
544fn coordinate_label(coordinate: &Coordinate) -> String {
545    format!("{} {}", coordinate.name, coordinate.version)
546}
547
548fn require_directory(path: &Path, label: &str) -> Result<(), String> {
549    let metadata = std::fs::symlink_metadata(path)
550        .map_err(|error| format!("inspect {label} {}: {error}", path.display()))?;
551    if metadata.is_dir() && !metadata.file_type().is_symlink() {
552        Ok(())
553    } else {
554        Err(format!("{label} is not an ordinary directory"))
555    }
556}
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561    use kcode_k1_web_bootstrap_archive::read;
562    use serde_json::Value;
563    use std::fs::File;
564    use std::io::Write;
565    use zip::write::SimpleFileOptions;
566
567    fn archive(root: &Path, name: &str, manifest: &str, entries: &[(&str, &[u8])]) -> Archive {
568        let path = root.join(name);
569        let file = File::create(&path).unwrap();
570        let mut zip = zip::ZipWriter::new(file);
571        zip.start_file("manifest.json", SimpleFileOptions::default())
572            .unwrap();
573        zip.write_all(manifest.as_bytes()).unwrap();
574        for (path, bytes) in entries {
575            zip.start_file(*path, SimpleFileOptions::default()).unwrap();
576            zip.write_all(bytes).unwrap();
577        }
578        zip.finish().unwrap();
579        read(&path).unwrap().archive
580    }
581
582    fn file<'a>(source: &'a SourcePackage, path: &str) -> &'a [u8] {
583        source
584            .files()
585            .iter()
586            .find(|file| file.path() == path)
587            .unwrap()
588            .bytes()
589    }
590
591    fn prepare_all(archive: &Archive, authority: AuthorityId) -> Vec<PreparedPackage> {
592        let plan = inspect(archive);
593        plan.order()
594            .iter()
595            .map(|index| prepare_package(&archive.packages()[*index], authority).unwrap())
596            .collect()
597    }
598
599    #[test]
600    fn prepares_complete_trees_current_routes_and_exact_metadata() {
601        let root = tempfile::tempdir().unwrap();
602        let manifest = r#"{"schema":1,"packages":[{"name":"dep","version":"1.5.0","entry":"index.js","tests":"tests.js","dependencies":[]},{"name":"dep","version":"2.1.0","entry":"index.js","tests":"tests.js","dependencies":[]},{"name":"bridge","version":"1.0.0","entry":"index.js","tests":"tests.js","dependencies":[{"name":"dep","selector":"2"}]},{"name":"app","version":"1.0.0","entry":"src/index.js","tests":"tests.js","dependencies":[{"name":"app","selector":"1"},{"name":"dep","selector":"1"}]}]}"#;
603        let entries: Vec<(&str, &[u8])> = vec![
604            (
605                "packages/dep/1.5.0/Documentation.md",
606                b"dep one docs\r\n",
607            ),
608            (
609                "packages/dep/1.5.0/index.js",
610                b"export const version=1;\n",
611            ),
612            (
613                "packages/dep/1.5.0/tests.js",
614                b"export function runTests(){}\n",
615            ),
616            (
617                "packages/dep/1.5.0/assets/deep/image.bin",
618                &[0, 1, 255],
619            ),
620            (
621                "packages/dep/2.1.0/Documentation.md",
622                b"dep two docs\n",
623            ),
624            (
625                "packages/dep/2.1.0/index.js",
626                b"export const version=2;\n",
627            ),
628            (
629                "packages/dep/2.1.0/tests.js",
630                b"export function runTests(){}\n",
631            ),
632            (
633                "packages/bridge/1.0.0/Documentation.md",
634                b"bridge docs\n",
635            ),
636            (
637                "packages/bridge/1.0.0/index.js",
638                b"import '/module/dep/v2/index.js';\nexport {};\n",
639            ),
640            (
641                "packages/bridge/1.0.0/tests.js",
642                b"export function runTests(){}\n",
643            ),
644            (
645                "packages/app/1.0.0/Documentation.md",
646                b"app docs\r\n",
647            ),
648            (
649                "packages/app/1.0.0/src/index.js",
650                b"import '/module/dep/v1/index.js';\nexport const self='/module/app/v1/src/index.js';\n",
651            ),
652            (
653                "packages/app/1.0.0/tests.js",
654                b"export function runTests(){}\n",
655            ),
656            (
657                "packages/app/1.0.0/styles/main.css",
658                b"@import '/module/dep/v1/style.css';\n",
659            ),
660        ];
661        let archive = archive(root.path(), "complete.zip", manifest, &entries);
662        let authority = AuthorityId::new(TxId::from_bytes([1; 12]));
663        let authority_text = authority.to_string();
664        let prepared = prepare_all(&archive, authority);
665
666        let dep_one = prepared
667            .iter()
668            .find(|item| {
669                item.coordinate.name == "dep" && item.coordinate.version == Version::new(1, 5, 0)
670            })
671            .unwrap();
672        assert_eq!(file(&dep_one.source, "assets/deep/image.bin"), [0, 1, 255]);
673
674        let app = prepared
675            .iter()
676            .find(|item| item.coordinate.name == "app")
677            .unwrap();
678        assert_eq!(app.source.entry(), "src/index.js");
679        assert_eq!(app.source.tests(), "tests.js");
680        assert_eq!(app.source.dependencies().len(), 1);
681        assert_eq!(app.source.dependencies()[0].name(), "dep");
682        let expected_documentation = format!(
683            "<!-- k1-web/v1\n{{\"dependencies\":[{{\"authority\":\"{authority_text}\",\"name\":\"dep\",\"selector\":\"1\"}}]}}\n-->\napp docs\r\n"
684        );
685        assert_eq!(
686            file(&app.source, "Documentation.md"),
687            expected_documentation.as_bytes()
688        );
689
690        let generated: Value = serde_json::from_slice(file(&app.source, "k1-web.json")).unwrap();
691        assert_eq!(generated["name"], "app");
692        assert_eq!(generated["version"], "1.0.0");
693        assert_eq!(generated["entry"], "src/index.js");
694        assert_eq!(generated["tests"], "tests.js");
695        assert_eq!(generated["dependencies"].as_array().unwrap().len(), 1);
696
697        let code = std::str::from_utf8(file(&app.source, "src/index.js")).unwrap();
698        assert!(code.contains(&format!("/{authority_text}/dep/v1/index.js")));
699        assert!(code.contains(&format!("/{authority_text}/app/v1/src/index.js")));
700        assert!(!code.contains("/module/"));
701
702        let style = std::str::from_utf8(file(&app.source, "styles/main.css")).unwrap();
703        assert!(style.contains(&format!("/{authority_text}/dep/v1/style.css")));
704    }
705
706    #[test]
707    fn warnings_do_not_prevent_preparation() {
708        let root = tempfile::tempdir().unwrap();
709        let manifest = r#"{"schema":1,"packages":[{"name":"a","version":"1.0.0","entry":"index.js","tests":"tests.js","dependencies":[{"name":"missing","selector":"1"}]},{"name":"b","version":"1.0.0","entry":"index.js","tests":"tests.js","dependencies":[{"name":"a","selector":"1"},{"name":"bad name","selector":"not a selector"}]},{"name":"c","version":"1.0.0","entry":"index.js","tests":"tests.js","dependencies":[{"name":"b","selector":"1"},{"name":"c","selector":"1"}]}]}"#;
710        let entries: Vec<(&str, &[u8])> = vec![
711            ("packages/a/1.0.0/Documentation.md", b"a docs\n"),
712            (
713                "packages/a/1.0.0/index.js",
714                b"import '/module/missing/v1/index.js';\n",
715            ),
716            (
717                "packages/a/1.0.0/tests.js",
718                b"export function runTests(){}\n",
719            ),
720            ("packages/b/1.0.0/Documentation.md", b"b docs\n"),
721            (
722                "packages/b/1.0.0/index.js",
723                b"import '/module/a/v1/index.js';\n",
724            ),
725            (
726                "packages/b/1.0.0/tests.js",
727                b"export function runTests(){}\n",
728            ),
729            ("packages/c/1.0.0/Documentation.md", b"c docs\n"),
730            (
731                "packages/c/1.0.0/index.js",
732                b"import '/module/b/v1/index.js';\n",
733            ),
734            (
735                "packages/c/1.0.0/tests.js",
736                b"export function runTests(){}\n",
737            ),
738        ];
739        let archive = archive(root.path(), "warnings.zip", manifest, &entries);
740        let plan = inspect(&archive);
741        assert!(!plan.warnings().is_empty());
742
743        let authority = AuthorityId::new(TxId::from_bytes([2; 12]));
744        let prepared = prepare_all(&archive, authority);
745        assert_eq!(prepared.len(), 3);
746
747        let a = prepared
748            .iter()
749            .find(|item| item.coordinate.name == "a")
750            .unwrap();
751        assert_eq!(a.source.dependencies().len(), 1);
752        assert_eq!(a.source.dependencies()[0].name(), "missing");
753
754        let b = prepared
755            .iter()
756            .find(|item| item.coordinate.name == "b")
757            .unwrap();
758        assert_eq!(b.source.dependencies().len(), 1);
759        assert_eq!(b.source.dependencies()[0].name(), "a");
760
761        let c = prepared
762            .iter()
763            .find(|item| item.coordinate.name == "c")
764            .unwrap();
765        assert_eq!(c.source.dependencies().len(), 1);
766        assert_eq!(c.source.dependencies()[0].name(), "b");
767    }
768
769    #[test]
770    fn preflight_log_contains_all_warnings_and_is_replaced() {
771        let root = tempfile::tempdir().unwrap();
772        let manifest = r#"{"schema":1,"packages":[{"name":"a","version":"1.0.0","entry":"index.js","tests":"tests.js","dependencies":[{"name":"missing-one","selector":"1"},{"name":"missing-two","selector":"2"}]}]}"#;
773        let entries: Vec<(&str, &[u8])> = vec![
774            ("packages/a/1.0.0/Documentation.md", b"a docs\n"),
775            ("packages/a/1.0.0/index.js", b"export {};\n"),
776            (
777                "packages/a/1.0.0/tests.js",
778                b"export function runTests(){}\n",
779            ),
780        ];
781        let archive = archive(root.path(), "log.zip", manifest, &entries);
782        let log = root.path().join("import.log");
783        std::fs::write(&log, "old contents\n").unwrap();
784
785        write_preflight_log(&archive, &log).unwrap();
786        let text = std::fs::read_to_string(log).unwrap();
787        assert!(!text.contains("old contents"));
788        assert_eq!(text.matches("WARN unresolved dependency").count(), 2);
789        assert!(text.contains("PREFLIGHT COMPLETE"));
790    }
791}