Fork of daniellemaywood.uk/gleam — Wasm codegen work
61 kB
1911 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2024 The Gleam contributors
3
4use std::collections::HashMap;
5
6use camino::{Utf8Path, Utf8PathBuf};
7use ecow::EcoString;
8use hexpm::version::Version;
9use pretty_assertions::assert_eq;
10
11use gleam_core::{
12 Error,
13 build::Runtime,
14 config::{DenoConfig, DenoFlag, Docs, ErlangConfig, JavaScriptConfig},
15 manifest::{Base16Checksum, Manifest, ManifestPackage, ManifestPackageSource},
16 paths::ProjectPaths,
17 requirement::Requirement,
18};
19
20use crate::dependencies::*;
21
22#[test]
23fn list_manifest_format() {
24 let mut buffer = vec![];
25 let manifest = Manifest {
26 requirements: HashMap::new(),
27 packages: vec![
28 ManifestPackage {
29 name: "root".into(),
30 version: Version::parse("1.0.0").unwrap(),
31 build_tools: ["gleam".into()].into(),
32 otp_app: None,
33 requirements: vec![],
34 source: ManifestPackageSource::Hex {
35 outer_checksum: Base16Checksum(vec![1, 2, 3, 4]),
36 },
37 },
38 ManifestPackage {
39 name: "aaa".into(),
40 version: Version::new(0, 4, 2),
41 build_tools: ["rebar3".into(), "make".into()].into(),
42 otp_app: Some("aaa_app".into()),
43 requirements: vec!["zzz".into(), "gleam_stdlib".into()],
44 source: ManifestPackageSource::Hex {
45 outer_checksum: Base16Checksum(vec![3, 22]),
46 },
47 },
48 ManifestPackage {
49 name: "zzz".into(),
50 version: Version::new(0, 4, 0),
51 build_tools: ["mix".into()].into(),
52 otp_app: None,
53 requirements: vec![],
54 source: ManifestPackageSource::Hex {
55 outer_checksum: Base16Checksum(vec![3, 22]),
56 },
57 },
58 ],
59 };
60 list_manifest_packages(&mut buffer, manifest).unwrap();
61 assert_eq!(
62 std::str::from_utf8(&buffer).unwrap(),
63 "Package Version
64------- -------
65root 1.0.0
66aaa 0.4.2
67zzz 0.4.0
68"
69 )
70}
71
72#[test]
73fn tree_format() {
74 let mut buffer = vec![];
75 let manifest = Manifest {
76 requirements: HashMap::new(),
77 packages: vec![
78 ManifestPackage {
79 name: "deps_proj".into(),
80 version: Version::parse("1.0.0").unwrap(),
81 build_tools: [].into(),
82 otp_app: None,
83 requirements: vec!["gleam_regexp".into(), "gleam_stdlib".into()],
84 source: ManifestPackageSource::Hex {
85 outer_checksum: Base16Checksum(vec![1, 2, 3, 4]),
86 },
87 },
88 ManifestPackage {
89 name: "gleam_stdlib".into(),
90 version: Version::new(0, 52, 0),
91 build_tools: ["rebar3".into(), "make".into()].into(),
92 otp_app: Some("aaa_app".into()),
93 requirements: vec![],
94 source: ManifestPackageSource::Hex {
95 outer_checksum: Base16Checksum(vec![3, 22]),
96 },
97 },
98 ManifestPackage {
99 name: "gleam_regexp".into(),
100 version: Version::new(1, 0, 0),
101 build_tools: ["mix".into()].into(),
102 otp_app: None,
103 requirements: vec!["gleam_stdlib".into()],
104 source: ManifestPackageSource::Hex {
105 outer_checksum: Base16Checksum(vec![3, 22]),
106 },
107 },
108 ],
109 };
110
111 let options = TreeOptions {
112 package: None,
113 invert: None,
114 };
115
116 let root_package_name = EcoString::from("deps_proj");
117
118 list_package_and_dependencies_tree(
119 &mut buffer,
120 options,
121 manifest.packages.clone(),
122 root_package_name,
123 )
124 .unwrap();
125 assert_eq!(
126 std::str::from_utf8(&buffer).unwrap(),
127 r#"deps_proj v1.0.0
128├── gleam_regexp v1.0.0
129│ └── gleam_stdlib v0.52.0
130└── gleam_stdlib v0.52.0
131"#
132 )
133}
134
135#[test]
136fn tree_package_format() {
137 let mut buffer = vec![];
138 let manifest = Manifest {
139 requirements: HashMap::new(),
140 packages: vec![
141 ManifestPackage {
142 name: "gleam_stdlib".into(),
143 version: Version::new(0, 52, 0),
144 build_tools: ["rebar3".into(), "make".into()].into(),
145 otp_app: Some("aaa_app".into()),
146 requirements: vec![],
147 source: ManifestPackageSource::Hex {
148 outer_checksum: Base16Checksum(vec![3, 22]),
149 },
150 },
151 ManifestPackage {
152 name: "deps_proj".into(),
153 version: Version::parse("1.0.0").unwrap(),
154 build_tools: [].into(),
155 otp_app: None,
156 requirements: vec!["gleam_stdlib".into(), "gleam_regexp".into()],
157 source: ManifestPackageSource::Hex {
158 outer_checksum: Base16Checksum(vec![1, 2, 3, 4]),
159 },
160 },
161 ManifestPackage {
162 name: "gleam_regexp".into(),
163 version: Version::new(1, 0, 0),
164 build_tools: ["mix".into()].into(),
165 otp_app: None,
166 requirements: vec!["gleam_stdlib".into()],
167 source: ManifestPackageSource::Hex {
168 outer_checksum: Base16Checksum(vec![3, 22]),
169 },
170 },
171 ],
172 };
173 let options = TreeOptions {
174 package: Some("gleam_regexp".to_string()),
175 invert: None,
176 };
177
178 let root_package_name = EcoString::from("deps_proj");
179
180 list_package_and_dependencies_tree(
181 &mut buffer,
182 options,
183 manifest.packages.clone(),
184 root_package_name,
185 )
186 .unwrap();
187 assert_eq!(
188 std::str::from_utf8(&buffer).unwrap(),
189 r#"gleam_regexp v1.0.0
190└── gleam_stdlib v0.52.0
191"#
192 )
193}
194
195#[test]
196fn tree_invert_format() {
197 let mut buffer = vec![];
198 let manifest = Manifest {
199 requirements: HashMap::new(),
200 packages: vec![
201 ManifestPackage {
202 name: "gleam_stdlib".into(),
203 version: Version::new(0, 52, 0),
204 build_tools: ["rebar3".into(), "make".into()].into(),
205 otp_app: Some("aaa_app".into()),
206 requirements: vec![],
207 source: ManifestPackageSource::Hex {
208 outer_checksum: Base16Checksum(vec![3, 22]),
209 },
210 },
211 ManifestPackage {
212 name: "deps_proj".into(),
213 version: Version::parse("1.0.0").unwrap(),
214 build_tools: [].into(),
215 otp_app: None,
216 requirements: vec!["gleam_stdlib".into(), "gleam_regexp".into()],
217 source: ManifestPackageSource::Hex {
218 outer_checksum: Base16Checksum(vec![1, 2, 3, 4]),
219 },
220 },
221 ManifestPackage {
222 name: "gleam_regexp".into(),
223 version: Version::new(1, 0, 0),
224 build_tools: ["mix".into()].into(),
225 otp_app: None,
226 requirements: vec!["gleam_stdlib".into()],
227 source: ManifestPackageSource::Hex {
228 outer_checksum: Base16Checksum(vec![3, 22]),
229 },
230 },
231 ],
232 };
233 let options = TreeOptions {
234 package: None,
235 invert: Some("gleam_stdlib".to_string()),
236 };
237
238 let root_package_name = EcoString::from("deps_proj");
239
240 list_package_and_dependencies_tree(
241 &mut buffer,
242 options,
243 manifest.packages.clone(),
244 root_package_name,
245 )
246 .unwrap();
247 assert_eq!(
248 std::str::from_utf8(&buffer).unwrap(),
249 r#"gleam_stdlib v0.52.0
250├── deps_proj v1.0.0
251└── gleam_regexp v1.0.0
252 └── deps_proj v1.0.0
253"#
254 )
255}
256
257#[test]
258fn list_tree_invalid_package_format() {
259 let mut buffer = vec![];
260 let manifest = Manifest {
261 requirements: HashMap::new(),
262 packages: vec![
263 ManifestPackage {
264 name: "gleam_stdlib".into(),
265 version: Version::new(0, 52, 0),
266 build_tools: ["rebar3".into(), "make".into()].into(),
267 otp_app: Some("aaa_app".into()),
268 requirements: vec![],
269 source: ManifestPackageSource::Hex {
270 outer_checksum: Base16Checksum(vec![3, 22]),
271 },
272 },
273 ManifestPackage {
274 name: "gleam_regexp".into(),
275 version: Version::new(1, 0, 0),
276 build_tools: ["mix".into()].into(),
277 otp_app: None,
278 requirements: vec!["gleam_stdlib".into()],
279 source: ManifestPackageSource::Hex {
280 outer_checksum: Base16Checksum(vec![3, 22]),
281 },
282 },
283 ManifestPackage {
284 name: "root".into(),
285 version: Version::parse("1.0.0").unwrap(),
286 build_tools: [].into(),
287 otp_app: None,
288 requirements: vec!["gleam_regexp".into(), "gleam_stdlib".into()],
289 source: ManifestPackageSource::Hex {
290 outer_checksum: Base16Checksum(vec![1, 2, 3, 4]),
291 },
292 },
293 ],
294 };
295 let options = TreeOptions {
296 package: Some("zzzzzz".to_string()),
297 invert: None,
298 };
299
300 let root_package_name = EcoString::from("deps_proj");
301
302 list_package_and_dependencies_tree(
303 &mut buffer,
304 options,
305 manifest.packages.clone(),
306 root_package_name,
307 )
308 .unwrap();
309 assert_eq!(
310 std::str::from_utf8(&buffer).unwrap(),
311 r#"Package not found. Please check the package name.
312"#
313 )
314}
315
316#[test]
317fn parse_gleam_add_specifier_invalid_semver() {
318 assert!(parse_gleam_add_specifier("some_package@1.2.3.4").is_err());
319}
320
321#[test]
322fn parse_gleam_add_specifier_non_numeric_version() {
323 assert!(parse_gleam_add_specifier("some_package@not_a_version").is_err());
324}
325
326#[test]
327fn parse_gleam_add_specifier_default() {
328 let provided = "some_package";
329 let expected = Requirement::hex(">= 0.0.0").unwrap();
330 let (package, version) = parse_gleam_add_specifier(provided).unwrap();
331 assert_eq!(version, expected);
332 assert_eq!("some_package", package);
333}
334
335#[test]
336fn parse_gleam_add_specifier_major_only() {
337 let provided = "wobble@1";
338 let expected = Requirement::hex(">= 1.0.0 and < 2.0.0").unwrap();
339 let (package, version) = parse_gleam_add_specifier(provided).unwrap();
340 assert_eq!(version, expected);
341 assert_eq!("wobble", package);
342}
343
344#[test]
345fn parse_gleam_add_specifier_major_and_minor() {
346 let provided = "wibble@1.2";
347 let expected = Requirement::hex(">= 1.2.0 and < 2.0.0").unwrap();
348 let (package, version) = parse_gleam_add_specifier(provided).unwrap();
349 assert_eq!(version, expected);
350 assert_eq!("wibble", package);
351}
352
353#[test]
354fn parse_gleam_add_specifier_major_minor_and_patch() {
355 let provided = "bobble@1.2.3";
356 let expected = Requirement::hex("1.2.3").unwrap();
357 let (package, version) = parse_gleam_add_specifier(provided).unwrap();
358 assert_eq!(version, expected);
359 assert_eq!("bobble", package);
360}
361
362#[test]
363fn missing_local_packages() {
364 let manifest = Manifest {
365 requirements: HashMap::new(),
366 packages: vec![
367 ManifestPackage {
368 name: "root".into(),
369 version: Version::parse("1.0.0").unwrap(),
370 build_tools: ["gleam".into()].into(),
371 otp_app: None,
372 requirements: vec![],
373 source: ManifestPackageSource::Hex {
374 outer_checksum: Base16Checksum(vec![1, 2, 3, 4]),
375 },
376 },
377 ManifestPackage {
378 name: "local1".into(),
379 version: Version::parse("1.0.0").unwrap(),
380 build_tools: ["gleam".into()].into(),
381 otp_app: None,
382 requirements: vec![],
383 source: ManifestPackageSource::Hex {
384 outer_checksum: Base16Checksum(vec![1, 2, 3, 4, 5]),
385 },
386 },
387 ManifestPackage {
388 name: "local2".into(),
389 version: Version::parse("3.0.0").unwrap(),
390 build_tools: ["gleam".into()].into(),
391 otp_app: None,
392 requirements: vec![],
393 source: ManifestPackageSource::Hex {
394 outer_checksum: Base16Checksum(vec![1, 2, 3, 4, 5]),
395 },
396 },
397 ],
398 };
399 let mut extra = LocalPackages {
400 packages: [
401 ("local2".into(), Version::parse("2.0.0").unwrap()),
402 ("local3".into(), Version::parse("3.0.0").unwrap()),
403 ]
404 .into(),
405 git: HashMap::new(),
406 }
407 .missing_local_packages(&manifest, "root");
408 extra.sort();
409 assert_eq!(
410 extra,
411 [
412 &ManifestPackage {
413 name: "local1".into(),
414 version: Version::parse("1.0.0").unwrap(),
415 build_tools: ["gleam".into()].into(),
416 otp_app: None,
417 requirements: vec![],
418 source: ManifestPackageSource::Hex {
419 outer_checksum: Base16Checksum(vec![1, 2, 3, 4, 5]),
420 },
421 },
422 &ManifestPackage {
423 name: "local2".into(),
424 version: Version::parse("3.0.0").unwrap(),
425 build_tools: ["gleam".into()].into(),
426 otp_app: None,
427 requirements: vec![],
428 source: ManifestPackageSource::Hex {
429 outer_checksum: Base16Checksum(vec![1, 2, 3, 4, 5]),
430 },
431 },
432 ]
433 )
434}
435
436#[test]
437fn extra_local_packages() {
438 let mut extra = LocalPackages {
439 packages: [
440 ("local1".into(), Version::parse("1.0.0").unwrap()),
441 ("local2".into(), Version::parse("2.0.0").unwrap()),
442 ("local3".into(), Version::parse("3.0.0").unwrap()),
443 ]
444 .into(),
445 git: HashMap::new(),
446 }
447 .extra_local_packages(&Manifest {
448 requirements: HashMap::new(),
449 packages: vec![
450 ManifestPackage {
451 name: "local1".into(),
452 version: Version::parse("1.0.0").unwrap(),
453 build_tools: ["gleam".into()].into(),
454 otp_app: None,
455 requirements: vec![],
456 source: ManifestPackageSource::Hex {
457 outer_checksum: Base16Checksum(vec![1, 2, 3, 4, 5]),
458 },
459 },
460 ManifestPackage {
461 name: "local2".into(),
462 version: Version::parse("3.0.0").unwrap(),
463 build_tools: ["gleam".into()].into(),
464 otp_app: None,
465 requirements: vec![],
466 source: ManifestPackageSource::Hex {
467 outer_checksum: Base16Checksum(vec![4, 5]),
468 },
469 },
470 ],
471 });
472 extra.sort();
473 assert_eq!(
474 extra,
475 [
476 ("local2".into(), Version::new(2, 0, 0)),
477 ("local3".into(), Version::new(3, 0, 0)),
478 ]
479 )
480}
481
482#[test]
483fn provide_wrong_package() {
484 let mut provided = HashMap::new();
485 let project_paths = crate::project_paths_at_current_directory_without_toml();
486 let result = provide_local_package(
487 "wrong_name".into(),
488 Utf8Path::new("./test/hello_world"),
489 Utf8Path::new("./"),
490 &project_paths,
491 &mut provided,
492 &mut vec!["root".into(), "subpackage".into()],
493 );
494 match result {
495 Err(Error::WrongDependencyProvided {
496 expected, found, ..
497 }) => {
498 assert_eq!(expected, "wrong_name");
499 assert_eq!(found, "hello_world");
500 }
501 _ => {
502 panic!("Expected WrongDependencyProvided error")
503 }
504 }
505}
506
507#[test]
508fn provide_existing_package() {
509 let mut provided = HashMap::new();
510 let project_paths = crate::project_paths_at_current_directory_without_toml();
511
512 let result = provide_local_package(
513 "hello_world".into(),
514 Utf8Path::new("./test/hello_world"),
515 Utf8Path::new("./"),
516 &project_paths,
517 &mut provided,
518 &mut vec!["root".into(), "subpackage".into()],
519 );
520 assert_eq!(
521 result,
522 Ok(hexpm::version::Range::new("== 0.1.0".into()).unwrap())
523 );
524
525 let result = provide_local_package(
526 "hello_world".into(),
527 Utf8Path::new("./test/hello_world"),
528 Utf8Path::new("./"),
529 &project_paths,
530 &mut provided,
531 &mut vec!["root".into(), "subpackage".into()],
532 );
533 assert_eq!(
534 result,
535 Ok(hexpm::version::Range::new("== 0.1.0".into()).unwrap())
536 );
537}
538
539#[test]
540fn provide_conflicting_package() {
541 let mut provided = HashMap::new();
542 let project_paths = crate::project_paths_at_current_directory_without_toml();
543 let result = provide_local_package(
544 "hello_world".into(),
545 Utf8Path::new("./test/hello_world"),
546 Utf8Path::new("./"),
547 &project_paths,
548 &mut provided,
549 &mut vec!["root".into(), "subpackage".into()],
550 );
551 assert_eq!(
552 result,
553 Ok(hexpm::version::Range::new("== 0.1.0".into()).unwrap())
554 );
555
556 let result = provide_package(
557 "hello_world".into(),
558 Utf8PathBuf::from("./test/other"),
559 SourceContext::Local {
560 path: Utf8Path::new("./test/other").to_path_buf(),
561 },
562 &project_paths,
563 &mut provided,
564 &mut vec!["root".into(), "subpackage".into()],
565 );
566 match result {
567 Err(Error::ProvidedDependencyConflict { package, .. }) => {
568 assert_eq!(package, "hello_world");
569 }
570 _ => {
571 panic!("Expected ProvidedDependencyConflict error")
572 }
573 }
574}
575
576#[test]
577fn provided_is_absolute() {
578 let mut provided = HashMap::new();
579 let project_paths = crate::project_paths_at_current_directory_without_toml();
580 let result = provide_local_package(
581 "hello_world".into(),
582 Utf8Path::new("./test/hello_world"),
583 Utf8Path::new("./"),
584 &project_paths,
585 &mut provided,
586 &mut vec!["root".into(), "subpackage".into()],
587 );
588 assert_eq!(
589 result,
590 Ok(hexpm::version::Range::new("== 0.1.0".into()).unwrap())
591 );
592 let package = provided.get("hello_world").unwrap().clone();
593 match package.source {
594 ProvidedPackageSource::Local { path } => {
595 assert!(path.is_absolute())
596 }
597 _ => {
598 panic!("Provide_local_package provided a package that is not local!")
599 }
600 }
601}
602
603#[test]
604fn provided_recursive() {
605 let mut provided = HashMap::new();
606 let project_paths = crate::project_paths_at_current_directory_without_toml();
607 let result = provide_local_package(
608 "hello_world".into(),
609 Utf8Path::new("./test/hello_world"),
610 Utf8Path::new("./"),
611 &project_paths,
612 &mut provided,
613 &mut vec!["root".into(), "hello_world".into(), "subpackage".into()],
614 );
615 assert_eq!(
616 result,
617 Err(Error::PackageCycle {
618 packages: vec!["subpackage".into(), "hello_world".into()],
619 })
620 )
621}
622
623#[test]
624fn provided_local_to_hex() {
625 let provided_package = ProvidedPackage {
626 version: Version::new(1, 0, 0),
627 source: ProvidedPackageSource::Local {
628 path: "canonical/path/to/package".into(),
629 },
630 requirements: [
631 (
632 "req_1".into(),
633 hexpm::version::Range::new("~> 1.0.0".into()).unwrap(),
634 ),
635 (
636 "req_2".into(),
637 hexpm::version::Range::new("== 1.0.0".into()).unwrap(),
638 ),
639 ]
640 .into(),
641 };
642
643 let hex_package = hexpm::Package {
644 name: "package".into(),
645 repository: "local".into(),
646 releases: vec![hexpm::Release {
647 version: Version::new(1, 0, 0),
648 retirement_status: None,
649 outer_checksum: vec![],
650 meta: (),
651 requirements: [
652 (
653 "req_1".into(),
654 hexpm::Dependency {
655 requirement: hexpm::version::Range::new("~> 1.0.0".into()).unwrap(),
656 optional: false,
657 app: None,
658 repository: None,
659 },
660 ),
661 (
662 "req_2".into(),
663 hexpm::Dependency {
664 requirement: hexpm::version::Range::new("== 1.0.0".into()).unwrap(),
665 optional: false,
666 app: None,
667 repository: None,
668 },
669 ),
670 ]
671 .into(),
672 }],
673 };
674
675 assert_eq!(
676 provided_package.to_hex_package(&"package".into()),
677 hex_package
678 );
679}
680
681#[test]
682fn provided_git_to_hex() {
683 let provided_package = ProvidedPackage {
684 version: Version::new(1, 0, 0),
685 source: ProvidedPackageSource::Git {
686 repo: "https://github.com/gleam-lang/gleam.git".into(),
687 commit: "bd9fe02f72250e6a136967917bcb1bdccaffa3c8".into(),
688 path: None,
689 },
690 requirements: [
691 (
692 "req_1".into(),
693 hexpm::version::Range::new("~> 1.0.0".into()).unwrap(),
694 ),
695 (
696 "req_2".into(),
697 hexpm::version::Range::new("== 1.0.0".into()).unwrap(),
698 ),
699 ]
700 .into(),
701 };
702
703 let hex_package = hexpm::Package {
704 name: "package".into(),
705 repository: "local".into(),
706 releases: vec![hexpm::Release {
707 version: Version::new(1, 0, 0),
708 retirement_status: None,
709 outer_checksum: vec![],
710 meta: (),
711 requirements: [
712 (
713 "req_1".into(),
714 hexpm::Dependency {
715 requirement: hexpm::version::Range::new("~> 1.0.0".into()).unwrap(),
716 optional: false,
717 app: None,
718 repository: None,
719 },
720 ),
721 (
722 "req_2".into(),
723 hexpm::Dependency {
724 requirement: hexpm::version::Range::new("== 1.0.0".into()).unwrap(),
725 optional: false,
726 app: None,
727 repository: None,
728 },
729 ),
730 ]
731 .into(),
732 }],
733 };
734
735 assert_eq!(
736 provided_package.to_hex_package(&"package".into()),
737 hex_package
738 );
739}
740
741#[test]
742fn provided_local_to_manifest() {
743 let provided_package = ProvidedPackage {
744 version: Version::new(1, 0, 0),
745 source: ProvidedPackageSource::Local {
746 path: "canonical/path/to/package".into(),
747 },
748 requirements: [
749 (
750 "req_1".into(),
751 hexpm::version::Range::new("~> 1.0.0".into()).unwrap(),
752 ),
753 (
754 "req_2".into(),
755 hexpm::version::Range::new("== 1.0.0".into()).unwrap(),
756 ),
757 ]
758 .into(),
759 };
760
761 let manifest_package = ManifestPackage {
762 name: "package".into(),
763 version: Version::new(1, 0, 0),
764 otp_app: None,
765 build_tools: vec!["gleam".into()],
766 requirements: vec!["req_1".into(), "req_2".into()],
767 source: ManifestPackageSource::Local {
768 path: "canonical/path/to/package".into(),
769 },
770 };
771
772 assert_eq!(
773 provided_package.to_manifest_package("package"),
774 manifest_package
775 );
776}
777
778#[test]
779fn provided_git_to_manifest() {
780 let provided_package = ProvidedPackage {
781 version: Version::new(1, 0, 0),
782 source: ProvidedPackageSource::Git {
783 repo: "https://github.com/gleam-lang/gleam.git".into(),
784 commit: "bd9fe02f72250e6a136967917bcb1bdccaffa3c8".into(),
785 path: None,
786 },
787 requirements: [
788 (
789 "req_1".into(),
790 hexpm::version::Range::new("~> 1.0.0".into()).unwrap(),
791 ),
792 (
793 "req_2".into(),
794 hexpm::version::Range::new("== 1.0.0".into()).unwrap(),
795 ),
796 ]
797 .into(),
798 };
799
800 let manifest_package = ManifestPackage {
801 name: "package".into(),
802 version: Version::new(1, 0, 0),
803 otp_app: None,
804 build_tools: vec!["gleam".into()],
805 requirements: vec!["req_1".into(), "req_2".into()],
806 source: ManifestPackageSource::Git {
807 repo: "https://github.com/gleam-lang/gleam.git".into(),
808 commit: "bd9fe02f72250e6a136967917bcb1bdccaffa3c8".into(),
809 path: None,
810 },
811 };
812
813 assert_eq!(
814 provided_package.to_manifest_package("package"),
815 manifest_package
816 );
817}
818
819#[test]
820fn provided_git_path_package_resolves_repo_relative_path() {
821 let repo_root = fs::canonicalise(Utf8Path::new("./test")).unwrap();
822 let result = resolve_git_path_package(
823 &"hello_world".into(),
824 Utf8Path::new("../hello_world"),
825 &"https://github.com/gleam-lang/wibble.git".into(),
826 &repo_root.join("hello_world"),
827 &repo_root,
828 );
829 assert_eq!(
830 result,
831 Ok((repo_root.join("hello_world"), "hello_world".into()))
832 );
833}
834
835#[test]
836fn provided_git_path_package_child_uses_canonical_parent_location() {
837 let tmp = tempfile::tempdir().unwrap();
838 let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap();
839 let repo = base.join("repo");
840 fs::mkdir(&repo.join("packages").join("package_a")).unwrap();
841 fs::mkdir(&repo.join("packages").join("package_b")).unwrap();
842 fs::write(
843 &repo.join("packages").join("package_a").join("gleam.toml"),
844 r#"
845name = "package_a"
846version = "0.1.0"
847
848[dependencies]
849package_b = { path = "../package_b" }
850"#,
851 )
852 .unwrap();
853 fs::write(
854 &repo.join("packages").join("package_b").join("gleam.toml"),
855 r#"
856name = "package_b"
857version = "0.2.0"
858"#,
859 )
860 .unwrap();
861
862 let mut provided = HashMap::new();
863 let project_paths = crate::project_paths_at_current_directory_without_toml();
864 let repo_root = fs::canonicalise(&repo).unwrap();
865 let result = provide_package(
866 "package_a".into(),
867 fs::canonicalise(&repo.join("packages").join("package_a")).unwrap(),
868 SourceContext::Git {
869 repo: "https://github.com/gleam-lang/wibble.git".into(),
870 commit: "95cd2c2f45907e5571e9b5fcdfb27ff35cdcdd29".into(),
871 path: Some("packages/package_a".into()),
872 repo_root: &repo_root,
873 },
874 &project_paths,
875 &mut provided,
876 &mut vec!["root".into()],
877 );
878 assert_eq!(
879 result,
880 Ok(hexpm::version::Range::new("== 0.1.0".into()).unwrap())
881 );
882 let package = provided.get("package_b").unwrap();
883 assert_eq!(
884 package.source,
885 ProvidedPackageSource::Git {
886 repo: "https://github.com/gleam-lang/wibble.git".into(),
887 commit: "95cd2c2f45907e5571e9b5fcdfb27ff35cdcdd29".into(),
888 path: Some("packages/package_b".into()),
889 }
890 );
891}
892
893#[test]
894fn provided_git_path_package_to_repo_root_has_no_path() {
895 let tmp = tempfile::tempdir().unwrap();
896 let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap();
897 let repo = base.join("repo");
898 fs::mkdir(&repo.join("backends").join("package_a")).unwrap();
899 fs::write(
900 &repo.join("gleam.toml"),
901 r#"
902name = "package_b"
903version = "0.2.0"
904"#,
905 )
906 .unwrap();
907 fs::write(
908 &repo.join("backends").join("package_a").join("gleam.toml"),
909 r#"
910name = "package_a"
911version = "0.1.0"
912
913[dependencies]
914package_b = { path = "../.." }
915"#,
916 )
917 .unwrap();
918
919 let mut provided = HashMap::new();
920 let project_paths = crate::project_paths_at_current_directory_without_toml();
921 let repo_root = fs::canonicalise(&repo).unwrap();
922 let result = provide_package(
923 "package_a".into(),
924 fs::canonicalise(&repo.join("backends").join("package_a")).unwrap(),
925 SourceContext::Git {
926 repo: "https://github.com/gleam-lang/wibble.git".into(),
927 commit: "95cd2c2f45907e5571e9b5fcdfb27ff35cdcdd29".into(),
928 path: Some("backends/package_a".into()),
929 repo_root: &repo_root,
930 },
931 &project_paths,
932 &mut provided,
933 &mut vec!["root".into()],
934 );
935 assert_eq!(
936 result,
937 Ok(hexpm::version::Range::new("== 0.1.0".into()).unwrap())
938 );
939 // A path dependency resolving to the repository root must match how the
940 // same package is provided when declared directly as a git dependency
941 // with no path (`None`), otherwise it is reported as a conflict.
942 let package = provided.get("package_b").unwrap();
943 assert_eq!(
944 package.source,
945 ProvidedPackageSource::Git {
946 repo: "https://github.com/gleam-lang/wibble.git".into(),
947 commit: "95cd2c2f45907e5571e9b5fcdfb27ff35cdcdd29".into(),
948 path: None,
949 }
950 );
951}
952
953#[test]
954fn provided_git_path_package_rejects_missing_directory() {
955 let tmp = tempfile::tempdir().unwrap();
956 let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap();
957 let repo = base.join("repo");
958 fs::mkdir(&repo.join("parent")).unwrap();
959
960 let repo_root = fs::canonicalise(&repo).unwrap();
961 let result = resolve_git_path_package(
962 &"missing".into(),
963 Utf8Path::new("../missing"),
964 &"https://github.com/gleam-lang/wibble.git".into(),
965 &repo_root.join("parent"),
966 &repo_root,
967 );
968 assert_eq!(
969 result,
970 Err(Error::GitDependencyPathNotFound {
971 package: "missing".into(),
972 path: "../missing".into(),
973 repo: "https://github.com/gleam-lang/wibble.git".into(),
974 })
975 );
976}
977
978#[test]
979fn resolve_git_subdir_accepts_directory_inside_repo() {
980 let tmp = tempfile::tempdir().unwrap();
981 let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap();
982 let clone = base.join("clone");
983 fs::mkdir(&clone.join("sub")).unwrap();
984
985 let expected = fs::canonicalise(&clone).unwrap().join("sub");
986 assert_eq!(
987 resolve_git_subdir(&clone, Utf8Path::new("sub")),
988 Some(expected)
989 );
990}
991
992#[test]
993fn resolve_git_subdir_rejects_missing_directory() {
994 let tmp = tempfile::tempdir().unwrap();
995 let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap();
996 let clone = base.join("clone");
997 fs::mkdir(&clone).unwrap();
998
999 assert_eq!(resolve_git_subdir(&clone, Utf8Path::new("missing")), None);
1000}
1001
1002#[cfg(unix)]
1003#[test]
1004fn resolve_git_subdir_rejects_symlink_escaping_repo() {
1005 let tmp = tempfile::tempdir().unwrap();
1006 let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap();
1007 let clone = base.join("clone");
1008 let outside = base.join("outside");
1009 fs::mkdir(&clone).unwrap();
1010 fs::mkdir(&outside).unwrap();
1011 std::os::unix::fs::symlink(outside.as_std_path(), clone.join("escape").as_std_path()).unwrap();
1012
1013 assert_eq!(resolve_git_subdir(&clone, Utf8Path::new("escape")), None);
1014}
1015
1016#[test]
1017fn provided_git_path_package_rejects_escaping_path() {
1018 let tmp = tempfile::tempdir().unwrap();
1019 let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap();
1020 let repo = base.join("repo");
1021 let outside = base.join("outside");
1022 fs::mkdir(&repo.join("parent")).unwrap();
1023 fs::mkdir(&outside).unwrap();
1024
1025 let repo_root = fs::canonicalise(&repo).unwrap();
1026 let result = resolve_git_path_package(
1027 &"package_a".into(),
1028 Utf8Path::new("../../outside"),
1029 &"https://github.com/gleam-lang/wibble.git".into(),
1030 &repo_root.join("parent"),
1031 &repo_root,
1032 );
1033 assert_eq!(
1034 result,
1035 Err(Error::GitDependencyPathNotFound {
1036 package: "package_a".into(),
1037 path: "../../outside".into(),
1038 repo: "https://github.com/gleam-lang/wibble.git".into(),
1039 })
1040 );
1041}
1042
1043#[cfg(unix)]
1044#[test]
1045fn provided_git_path_package_rejects_symlink_escaping_repo() {
1046 let tmp = tempfile::tempdir().unwrap();
1047 let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap();
1048 let repo = base.join("repo");
1049 let outside = base.join("outside");
1050 fs::mkdir(&repo.join("parent")).unwrap();
1051 fs::mkdir(&outside).unwrap();
1052 std::os::unix::fs::symlink(outside.as_std_path(), repo.join("escape").as_std_path()).unwrap();
1053
1054 let repo_root = fs::canonicalise(&repo).unwrap();
1055 let result = resolve_git_path_package(
1056 &"escape".into(),
1057 Utf8Path::new("../escape"),
1058 &"https://github.com/gleam-lang/wibble.git".into(),
1059 &repo_root.join("parent"),
1060 &repo_root,
1061 );
1062 assert_eq!(
1063 result,
1064 Err(Error::GitDependencyPathNotFound {
1065 package: "escape".into(),
1066 path: "../escape".into(),
1067 repo: "https://github.com/gleam-lang/wibble.git".into(),
1068 })
1069 );
1070}
1071
1072#[test]
1073fn git_repo_dir_name_uses_repo_basename_and_hash_of_full_url() {
1074 assert_eq!(
1075 git_repo_dir_name("https://github.com/gleam-lang/gleam"),
1076 "gleam-4d40009bf5eb0110"
1077 );
1078 assert_eq!(
1079 git_repo_dir_name("https://github.com/gleam-lang/gleam.git"),
1080 "gleam-ea2aeaa3761e8bc6"
1081 );
1082 assert_eq!(
1083 git_repo_dir_name("git@github.com:gleam-lang/gleam.git"),
1084 "gleam-102c9e1e5cf87965"
1085 );
1086 assert_eq!(
1087 git_repo_dir_name("C:\\src\\gleam.git"),
1088 "gleam-ab03c1f252f639da"
1089 );
1090}
1091
1092#[test]
1093fn git_staging_path_is_clone_path_with_staging_suffix() {
1094 let paths = ProjectPaths::new("/app".into());
1095 let repo = "https://github.com/gleam-lang/gleam.git";
1096 assert_eq!(
1097 paths.build_git_repo(&git_repo_dir_name(repo)),
1098 Utf8PathBuf::from("/app/build/git/gleam-ea2aeaa3761e8bc6")
1099 );
1100 assert_eq!(
1101 git_staging_path(&paths, repo, "wibble"),
1102 Utf8PathBuf::from("/app/build/git/gleam-ea2aeaa3761e8bc6-wibble-staging")
1103 );
1104}
1105
1106#[test]
1107fn git_checkout_cleanup_deletes_staging_directory() {
1108 let tmp = tempfile::tempdir().unwrap();
1109 let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap();
1110 let staging_path = base.join("clone-staging");
1111 fs::mkdir(&staging_path).unwrap();
1112 fs::write(&staging_path.join("gleam.toml"), "name = \"wibble\"").unwrap();
1113
1114 let checkout = GitCheckout::Staged {
1115 commit: "95cd2c2f45907e5571e9b5fcdfb27ff35cdcdd29".into(),
1116 staging_path: staging_path.clone(),
1117 };
1118 assert_eq!(checkout.cleanup(), Ok(()));
1119
1120 assert!(!staging_path.exists());
1121}
1122
1123#[test]
1124fn git_checkout_cleanup_without_staging_is_noop() {
1125 let checkout = GitCheckout::InPlace {
1126 commit: "95cd2c2f45907e5571e9b5fcdfb27ff35cdcdd29".into(),
1127 };
1128 assert_eq!(checkout.cleanup(), Ok(()));
1129}
1130
1131#[test]
1132fn remove_unused_git_clones_sweeps_unexpected_directories() {
1133 let tmp = tempfile::tempdir().unwrap();
1134 let root = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap();
1135 let paths = ProjectPaths::new(root);
1136 let repo = "https://github.com/gleam-lang/gleam.git";
1137
1138 let expected_clone = paths.build_git_repo(&git_repo_dir_name(repo));
1139 let stale_clone = paths.build_git_repo("other-0011223344556677");
1140 let stale_staging = paths.build_git_repo(&format!("{}-staging", git_repo_dir_name(repo)));
1141 fs::mkdir(&expected_clone).unwrap();
1142 fs::mkdir(&stale_clone).unwrap();
1143 fs::mkdir(&stale_staging).unwrap();
1144
1145 let manifest = Manifest {
1146 requirements: HashMap::new(),
1147 packages: vec![ManifestPackage {
1148 name: "wibble".into(),
1149 version: Version::new(1, 0, 0),
1150 build_tools: ["gleam".into()].into(),
1151 otp_app: None,
1152 requirements: vec![],
1153 source: ManifestPackageSource::Git {
1154 repo: repo.into(),
1155 commit: "95cd2c2f45907e5571e9b5fcdfb27ff35cdcdd29".into(),
1156 path: Some("wibble".into()),
1157 },
1158 }],
1159 };
1160
1161 assert_eq!(remove_unused_git_clones(&paths, &manifest), Ok(()));
1162
1163 assert!(expected_clone.is_dir());
1164 assert!(!stale_clone.exists());
1165 assert!(!stale_staging.exists());
1166}
1167
1168#[test]
1169fn remove_unused_git_clones_missing_directory_is_noop() {
1170 let tmp = tempfile::tempdir().unwrap();
1171 let root = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap();
1172 let paths = ProjectPaths::new(root);
1173 let manifest = Manifest {
1174 requirements: HashMap::new(),
1175 packages: vec![],
1176 };
1177
1178 assert_eq!(remove_unused_git_clones(&paths, &manifest), Ok(()));
1179}
1180
1181#[test]
1182fn verified_requirements_equality_with_canonicalized_paths() {
1183 let temp_dir = tempfile::tempdir().expect("Failed to create a temp directory");
1184 let temp_path = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf())
1185 .expect("Path should be valid UTF-8");
1186
1187 let sub_dir = temp_path.join("subdir");
1188 std::fs::create_dir(&sub_dir).expect("Failed to create a subdir");
1189 let file_path = sub_dir.join("file.txt");
1190 fs::write(&file_path, "content").expect("Failed to write to file");
1191
1192 let canonical_path = std::fs::canonicalize(&file_path).expect("Failed to canonicalize path");
1193 let relative_path = temp_path.join("./subdir/../subdir/./file.txt");
1194
1195 let requirements1 = HashMap::from([(
1196 EcoString::from("dep1"),
1197 Requirement::Path {
1198 path: Utf8PathBuf::from(canonical_path.to_str().expect("Path should be valid UTF-8")),
1199 },
1200 )]);
1201
1202 let requirements2 = HashMap::from([(
1203 EcoString::from("dep1"),
1204 Requirement::Path {
1205 path: Utf8PathBuf::from(relative_path.to_string()),
1206 },
1207 )]);
1208
1209 assert!(
1210 is_same_requirements(&requirements1, &requirements2, &temp_path)
1211 .expect("Requirements should be the same")
1212 );
1213}
1214
1215#[test]
1216fn test_path_dependency_config_updates() {
1217 let temp_dir = tempfile::tempdir().expect("Failed to create a temp directory");
1218 let root_path = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf())
1219 .expect("Path should be valid UTF-8");
1220 let paths = ProjectPaths::new(root_path.clone());
1221 let dep_path = root_path.join("dep");
1222 std::fs::create_dir_all(&dep_path).expect("Failed to create dependency directory");
1223 let build_packages_dir = root_path.join("build").join("packages");
1224 std::fs::create_dir_all(&build_packages_dir)
1225 .expect("Failed to create build/packages directory");
1226
1227 let config = "name = \"dep\"
1228version = \"1.0.0\"
1229
1230[dependencies]
1231";
1232 let dep_config_path = dep_path.join("gleam.toml");
1233 fs::write(&dep_config_path, config).expect("Failed to write to manifest file");
1234
1235 let requirements = HashMap::from([(
1236 EcoString::from("dep"),
1237 Requirement::Path {
1238 path: Utf8PathBuf::from("dep"),
1239 },
1240 )]);
1241
1242 // Initial check testing
1243
1244 let unchanged = path_dependency_configs_unchanged(&requirements, &paths).unwrap();
1245 assert!(!unchanged, "fresh always needs resolution");
1246
1247 let fingerprint_path = build_packages_dir.join("dep.config_fingerprint");
1248 assert!(fingerprint_path.exists(), "fingerprint must exist");
1249
1250 let unchanged = path_dependency_configs_unchanged(&requirements, &paths).unwrap();
1251 assert!(unchanged, "is unchanged");
1252
1253 // Set fingerprint mtime to some time in the past. This causes the mtime check to fail,
1254 // so it moves on to checking the fingerprint itself.
1255 let past = std::time::SystemTime::now() - std::time::Duration::from_secs(10);
1256 let past = filetime::FileTime::from_system_time(past);
1257 filetime::set_file_mtime(&fingerprint_path, past).unwrap();
1258
1259 let unchanged = path_dependency_configs_unchanged(&requirements, &paths).unwrap();
1260 assert!(unchanged, "mtime is outdated, but content has not changed");
1261
1262 // Writing new content means the mtime and the fingerprint checks will fail.
1263 let config = "name = \"dep\"
1264version = \"1.0.0\"
1265
1266[dependencies]
1267blah = \">= 1.0.0\"
1268";
1269 fs::write(&dep_config_path, config).unwrap();
1270
1271 let unchanged = path_dependency_configs_unchanged(&requirements, &paths).unwrap();
1272 assert!(!unchanged, "content has changed");
1273
1274 // Run again, to ensure that the fingerprint has been updated.
1275 let unchanged = path_dependency_configs_unchanged(&requirements, &paths).unwrap();
1276 assert!(unchanged, "no changes since last run");
1277
1278 // Test that mtime is checked first, and that content is not checked when the mtime
1279 // is still valid. We do this by having having content that would fail the fingerprint
1280 // check, but having a fresh mtime so it never gets checked.
1281 // This can never happen in reality as you can't update the content without updating
1282 // the mtime, but we create the situation in this test to verify the short-circuiting
1283 // behaviour.
1284 let config = "name = \"dep\"
1285version = \"1.0.0\"
1286
1287[dependencies]
1288blah = \">= 1.0.0\"
1289wub = \">= 1.0.0\"
1290";
1291 fs::write(&dep_config_path, config).unwrap();
1292 let future = std::time::SystemTime::now() + std::time::Duration::from_secs(10);
1293 let future = filetime::FileTime::from_system_time(future);
1294 filetime::set_file_mtime(&fingerprint_path, future).unwrap();
1295
1296 let unchanged = path_dependency_configs_unchanged(&requirements, &paths).unwrap();
1297 assert!(unchanged, "fingerprint is outdated, but mtime is not");
1298}
1299
1300fn create_testable_unlock_manifest(
1301 packages: Vec<(EcoString, Version, Vec<EcoString>)>,
1302 requirements: Vec<(EcoString, EcoString)>,
1303) -> Manifest {
1304 let manifest_packages = packages
1305 .into_iter()
1306 .map(|(name, version, requirements)| ManifestPackage {
1307 name,
1308 version,
1309 build_tools: vec!["gleam".into()],
1310 otp_app: None,
1311 requirements,
1312 source: ManifestPackageSource::Hex {
1313 outer_checksum: Base16Checksum(vec![]),
1314 },
1315 })
1316 .collect();
1317
1318 let root_requirements = requirements
1319 .into_iter()
1320 .map(|(name, range)| {
1321 (
1322 name,
1323 Requirement::Hex {
1324 version: hexpm::version::Range::new(range.into()).unwrap(),
1325 },
1326 )
1327 })
1328 .collect();
1329
1330 Manifest {
1331 packages: manifest_packages,
1332 requirements: root_requirements,
1333 }
1334}
1335
1336#[test]
1337fn test_unlock_package() {
1338 let mut locked = HashMap::from([
1339 ("package_a".into(), Version::new(1, 0, 0)),
1340 ("package_b".into(), Version::new(2, 0, 0)),
1341 ("package_c".into(), Version::new(3, 0, 0)),
1342 ("package_d".into(), Version::new(4, 0, 0)),
1343 ]);
1344
1345 let packages = vec![
1346 (
1347 "package_a".into(),
1348 Version::new(1, 0, 0),
1349 vec!["package_b".into()],
1350 ),
1351 (
1352 "package_b".into(),
1353 Version::new(2, 0, 0),
1354 vec!["package_c".into()],
1355 ),
1356 ("package_c".into(), Version::new(3, 0, 0), vec![]),
1357 ("package_d".into(), Version::new(4, 0, 0), vec![]),
1358 ];
1359
1360 let manifest = create_testable_unlock_manifest(packages, Vec::new());
1361
1362 let packages_to_unlock = vec!["package_a".into()];
1363 unlock_packages(&mut locked, &packages_to_unlock, Some(&manifest)).unwrap();
1364
1365 assert!(!locked.contains_key("package_a"));
1366 assert!(!locked.contains_key("package_b"));
1367 assert!(!locked.contains_key("package_c"));
1368 assert!(locked.contains_key("package_d"));
1369}
1370
1371#[test]
1372fn test_unlock_package_without_manifest() {
1373 let mut locked = HashMap::from([
1374 ("package_a".into(), Version::new(1, 0, 0)),
1375 ("package_b".into(), Version::new(2, 0, 0)),
1376 ("package_c".into(), Version::new(3, 0, 0)),
1377 ]);
1378
1379 let packages_to_unlock = vec!["package_a".into()];
1380 unlock_packages(&mut locked, &packages_to_unlock, None).unwrap();
1381
1382 assert!(!locked.contains_key("package_a"));
1383 assert!(locked.contains_key("package_b"));
1384 assert!(locked.contains_key("package_c"));
1385}
1386
1387#[test]
1388fn test_unlock_nonexistent_package() {
1389 let initial_locked = HashMap::from([
1390 ("package_a".into(), Version::new(1, 0, 0)),
1391 ("package_b".into(), Version::new(2, 0, 0)),
1392 ]);
1393
1394 let packages = vec![
1395 (
1396 "package_a".into(),
1397 Version::new(1, 0, 0),
1398 vec!["package_b".into()],
1399 ),
1400 ("package_b".into(), Version::new(2, 0, 0), vec![]),
1401 ];
1402
1403 let manifest = create_testable_unlock_manifest(packages, Vec::new());
1404
1405 let packages_to_unlock = vec!["nonexistent_package".into()];
1406 let mut locked = initial_locked.clone();
1407 unlock_packages(&mut locked, &packages_to_unlock, Some(&manifest)).unwrap();
1408
1409 assert_eq!(
1410 initial_locked, locked,
1411 "Locked packages should remain unchanged"
1412 );
1413}
1414
1415#[test]
1416fn test_unlock_multiple_packages() {
1417 let mut locked = HashMap::from([
1418 ("package_a".into(), Version::new(1, 0, 0)),
1419 ("package_b".into(), Version::new(2, 0, 0)),
1420 ("package_c".into(), Version::new(3, 0, 0)),
1421 ("package_d".into(), Version::new(4, 0, 0)),
1422 ("package_e".into(), Version::new(5, 0, 0)),
1423 ]);
1424
1425 let packages = vec![
1426 (
1427 "package_a".into(),
1428 Version::new(1, 0, 0),
1429 vec!["package_b".into()],
1430 ),
1431 (
1432 "package_b".into(),
1433 Version::new(2, 0, 0),
1434 vec!["package_c".into()],
1435 ),
1436 ("package_c".into(), Version::new(3, 0, 0), vec![]),
1437 (
1438 "package_d".into(),
1439 Version::new(4, 0, 0),
1440 vec!["package_e".into()],
1441 ),
1442 ("package_e".into(), Version::new(5, 0, 0), vec![]),
1443 ];
1444
1445 let manifest = create_testable_unlock_manifest(packages, Vec::new());
1446
1447 let packages_to_unlock = vec!["package_a".into(), "package_d".into()];
1448 unlock_packages(&mut locked, &packages_to_unlock, Some(&manifest)).unwrap();
1449
1450 assert!(!locked.contains_key("package_a"));
1451 assert!(!locked.contains_key("package_b"));
1452 assert!(!locked.contains_key("package_c"));
1453 assert!(!locked.contains_key("package_d"));
1454 assert!(!locked.contains_key("package_e"));
1455}
1456
1457#[test]
1458fn test_unlock_packages_empty_input() {
1459 let initial_locked = HashMap::from([
1460 ("package_a".into(), Version::new(1, 0, 0)),
1461 ("package_b".into(), Version::new(2, 0, 0)),
1462 ]);
1463
1464 let packages = vec![
1465 (
1466 "package_a".into(),
1467 Version::new(1, 0, 0),
1468 vec!["package_b".into()],
1469 ),
1470 ("package_b".into(), Version::new(2, 0, 0), vec![]),
1471 ];
1472
1473 let manifest = create_testable_unlock_manifest(packages, Vec::new());
1474
1475 let packages_to_unlock: Vec<EcoString> = vec![];
1476 let mut locked = initial_locked.clone();
1477 unlock_packages(&mut locked, &packages_to_unlock, Some(&manifest)).unwrap();
1478
1479 assert_eq!(
1480 initial_locked, locked,
1481 "Locked packages should remain unchanged when no packages are specified to unlock"
1482 );
1483}
1484
1485#[test]
1486fn test_unlock_package_preserve_shared_deps() {
1487 let mut locked = HashMap::from([
1488 ("package_a".into(), Version::new(1, 0, 0)),
1489 ("package_b".into(), Version::new(2, 0, 0)),
1490 ("package_c".into(), Version::new(3, 0, 0)),
1491 ]);
1492
1493 let packages = vec![
1494 (
1495 "package_a".into(),
1496 Version::new(1, 0, 0),
1497 vec!["package_c".into()],
1498 ),
1499 (
1500 "package_b".into(),
1501 Version::new(2, 0, 0),
1502 vec!["package_c".into()],
1503 ),
1504 ("package_c".into(), Version::new(3, 0, 0), vec![]),
1505 ];
1506
1507 let manifest = create_testable_unlock_manifest(packages, Vec::new());
1508
1509 let packages_to_unlock: Vec<EcoString> = vec!["package_a".into()];
1510 unlock_packages(&mut locked, &packages_to_unlock, Some(&manifest)).unwrap();
1511
1512 assert!(!locked.contains_key("package_a"));
1513 assert!(locked.contains_key("package_b"));
1514 assert!(locked.contains_key("package_c"));
1515}
1516
1517#[test]
1518fn test_unlock_package_with_root_dep() {
1519 let mut locked = HashMap::from([
1520 ("package_a".into(), Version::new(1, 0, 0)),
1521 ("package_b".into(), Version::new(2, 0, 0)),
1522 ("package_c".into(), Version::new(3, 0, 0)),
1523 ]);
1524
1525 let packages = vec![
1526 (
1527 "package_a".into(),
1528 Version::new(1, 0, 0),
1529 vec!["package_b".into()],
1530 ),
1531 (
1532 "package_b".into(),
1533 Version::new(2, 0, 0),
1534 vec!["package_c".into()],
1535 ),
1536 ("package_c".into(), Version::new(3, 0, 0), vec![]),
1537 ];
1538
1539 let requirements = vec![("package_b".into(), ">= 2.0.0".into())];
1540
1541 let manifest = create_testable_unlock_manifest(packages, requirements);
1542
1543 let packages_to_unlock: Vec<EcoString> = vec!["package_a".into()];
1544 unlock_packages(&mut locked, &packages_to_unlock, Some(&manifest)).unwrap();
1545
1546 assert!(!locked.contains_key("package_a"));
1547 assert!(locked.contains_key("package_b"));
1548 assert!(locked.contains_key("package_c"));
1549}
1550
1551#[test]
1552fn test_unlock_root_dep_package() {
1553 let mut locked = HashMap::from([
1554 ("package_a".into(), Version::new(1, 0, 0)),
1555 ("package_b".into(), Version::new(2, 0, 0)),
1556 ("package_c".into(), Version::new(3, 0, 0)),
1557 ]);
1558
1559 let packages = vec![
1560 (
1561 "package_a".into(),
1562 Version::new(1, 0, 0),
1563 vec!["package_b".into()],
1564 ),
1565 ("package_b".into(), Version::new(2, 0, 0), vec![]),
1566 ("package_c".into(), Version::new(3, 0, 0), vec![]),
1567 ];
1568
1569 let requirements = vec![("package_a".into(), ">= 1.0.0".into())];
1570
1571 let manifest = create_testable_unlock_manifest(packages, requirements);
1572
1573 let packages_to_unlock: Vec<EcoString> = vec!["package_a".into()];
1574 unlock_packages(&mut locked, &packages_to_unlock, Some(&manifest)).unwrap();
1575
1576 assert!(!locked.contains_key("package_a"));
1577 assert!(!locked.contains_key("package_b"));
1578 assert!(locked.contains_key("package_c"));
1579}
1580
1581#[test]
1582fn test_unlock_package_with_and_without_root_dep() {
1583 let mut locked = HashMap::from([
1584 ("package_a".into(), Version::new(1, 0, 0)),
1585 ("package_b".into(), Version::new(2, 0, 0)),
1586 ("package_c".into(), Version::new(3, 0, 0)),
1587 ]);
1588
1589 let packages = vec![
1590 (
1591 "package_a".into(),
1592 Version::new(1, 0, 0),
1593 vec!["package_b".into(), "package_c".into()],
1594 ),
1595 ("package_b".into(), Version::new(2, 0, 0), vec![]),
1596 ("package_c".into(), Version::new(3, 0, 0), vec![]),
1597 ];
1598
1599 let requirements = vec![("package_b".into(), ">= 2.0.0".into())];
1600
1601 let manifest = create_testable_unlock_manifest(packages, requirements);
1602
1603 let packages_to_unlock: Vec<EcoString> = vec!["package_a".into()];
1604 unlock_packages(&mut locked, &packages_to_unlock, Some(&manifest)).unwrap();
1605
1606 assert!(!locked.contains_key("package_a"));
1607 assert!(locked.contains_key("package_b"));
1608 assert!(!locked.contains_key("package_c"));
1609}
1610
1611fn manifest_package(name: &str, version: &str, requirements: Vec<EcoString>) -> ManifestPackage {
1612 ManifestPackage {
1613 name: name.into(),
1614 version: Version::parse(version).unwrap(),
1615 build_tools: ["gleam".into()].into(),
1616 otp_app: None,
1617 requirements,
1618 source: ManifestPackageSource::Hex {
1619 outer_checksum: Base16Checksum(vec![1, 2, 3, 4]),
1620 },
1621 }
1622}
1623
1624fn package_config(
1625 dependencies: HashMap<EcoString, Requirement>,
1626 dev_dependencies: HashMap<EcoString, Requirement>,
1627) -> PackageConfig {
1628 PackageConfig {
1629 name: "the_package".into(),
1630 version: Version::parse("1.0.0").unwrap(),
1631 gleam_version: None,
1632 licences: vec![],
1633 description: "".into(),
1634 documentation: Docs { pages: vec![] },
1635 dependencies,
1636 dev_dependencies,
1637 repository: None,
1638 links: vec![],
1639 erlang: ErlangConfig {
1640 application_start_module: None,
1641 application_start_argument: None,
1642 extra_applications: vec![],
1643 },
1644 javascript: JavaScriptConfig {
1645 typescript_declarations: false,
1646 source_maps: false,
1647 runtime: Runtime::NodeJs,
1648 deno: DenoConfig {
1649 allow_env: DenoFlag::AllowAll,
1650 allow_sys: true,
1651 allow_hrtime: true,
1652 allow_net: DenoFlag::AllowAll,
1653 allow_ffi: true,
1654 allow_read: DenoFlag::AllowAll,
1655 allow_run: DenoFlag::AllowAll,
1656 allow_write: DenoFlag::AllowAll,
1657 allow_all: true,
1658 unstable: true,
1659 location: None,
1660 },
1661 },
1662 target: Target::Erlang,
1663 internal_modules: None,
1664 }
1665}
1666
1667#[test]
1668fn test_remove_do_nothing() {
1669 let config = package_config(
1670 HashMap::from([("a".into(), Requirement::hex("~>1.0").unwrap())]),
1671 HashMap::from([("b".into(), Requirement::hex("~>2.0").unwrap())]),
1672 );
1673
1674 let mut manifest = Manifest {
1675 requirements: HashMap::from([
1676 ("a".into(), Requirement::hex("~>1.0").unwrap()),
1677 ("b".into(), Requirement::hex("~>2.0").unwrap()),
1678 ]),
1679 packages: vec![
1680 manifest_package("a", "1.0.0", vec![]),
1681 manifest_package("b", "2.0.8", vec![]),
1682 ],
1683 };
1684
1685 let manifest_copy = manifest.clone();
1686
1687 remove_extra_requirements(&config, &mut manifest).unwrap();
1688
1689 assert_eq!(manifest.requirements, manifest_copy.requirements);
1690 assert_eq!(manifest.packages, manifest_copy.packages);
1691}
1692
1693#[test]
1694fn test_remove_simple() {
1695 let config = package_config(HashMap::new(), HashMap::new());
1696
1697 let mut manifest = Manifest {
1698 requirements: HashMap::from([("a".into(), Requirement::hex("~>1.0").unwrap())]),
1699 packages: vec![manifest_package("a", "1.0.0", vec![])],
1700 };
1701
1702 remove_extra_requirements(&config, &mut manifest).unwrap();
1703
1704 assert_eq!(manifest.requirements, config.dependencies);
1705 assert_eq!(manifest.packages, vec![]);
1706}
1707
1708#[test]
1709fn test_remove_package_with_transitive_dependencies() {
1710 let config = package_config(HashMap::new(), HashMap::new());
1711
1712 let mut manifest = Manifest {
1713 requirements: HashMap::from([("a".into(), Requirement::hex("~>1.0").unwrap())]),
1714 packages: vec![
1715 manifest_package("a", "1.0.0", vec!["b".into()]),
1716 manifest_package("b", "1.2.3", vec!["c".into()]),
1717 manifest_package("c", "2.0.0", vec![]),
1718 ],
1719 };
1720
1721 remove_extra_requirements(&config, &mut manifest).unwrap();
1722
1723 assert_eq!(manifest.requirements, config.dependencies);
1724 assert_eq!(manifest.packages, vec![]);
1725}
1726
1727#[test]
1728fn test_remove_package_with_shared_transitive_dependencies() {
1729 let config = package_config(
1730 HashMap::from([("a".into(), Requirement::hex("~>1.0").unwrap())]),
1731 HashMap::new(),
1732 );
1733
1734 let mut manifest = Manifest {
1735 requirements: HashMap::from([
1736 ("a".into(), Requirement::hex("~>1.0").unwrap()),
1737 ("b".into(), Requirement::hex("~>1.0").unwrap()),
1738 ]),
1739 packages: vec![
1740 manifest_package("a", "1.0.0", vec!["c".into()]),
1741 manifest_package("b", "1.2.3", vec!["c".into(), "d".into()]),
1742 manifest_package("c", "2.0.0", vec![]),
1743 manifest_package("d", "0.1.0", vec![]),
1744 ],
1745 };
1746
1747 remove_extra_requirements(&config, &mut manifest).unwrap();
1748
1749 assert_eq!(manifest.requirements, config.dependencies);
1750 assert_eq!(
1751 manifest.packages,
1752 vec![
1753 manifest_package("a", "1.0.0", vec!["c".into()]),
1754 manifest_package("c", "2.0.0", vec![]),
1755 ]
1756 );
1757}
1758
1759#[test]
1760fn test_remove_package_that_is_also_a_transitive_dependency() {
1761 let config = package_config(
1762 HashMap::from([("a".into(), Requirement::hex("~>1.0").unwrap())]),
1763 HashMap::new(),
1764 );
1765
1766 let mut manifest = Manifest {
1767 requirements: HashMap::from([
1768 ("a".into(), Requirement::hex("~>1.0").unwrap()),
1769 ("b".into(), Requirement::hex("~>1.0").unwrap()),
1770 ]),
1771 packages: vec![
1772 manifest_package("a", "1.0.0", vec!["b".into(), "c".into()]),
1773 manifest_package("b", "1.2.3", vec!["c".into(), "d".into()]),
1774 manifest_package("c", "2.0.0", vec![]),
1775 manifest_package("d", "0.1.0", vec![]),
1776 ],
1777 };
1778
1779 let manifest_copy = manifest.clone();
1780
1781 remove_extra_requirements(&config, &mut manifest).unwrap();
1782
1783 assert_eq!(manifest.requirements, config.dependencies);
1784 assert_eq!(manifest.packages, manifest_copy.packages);
1785}
1786
1787#[test]
1788fn test_pretty_print_major_versions_available() {
1789 let versions = vec![
1790 (
1791 "very_long_package_name".to_string(),
1792 (Version::new(18, 382, 43), Version::new(19, 0, 38)),
1793 ),
1794 (
1795 "gleam_stdlib".to_string(),
1796 (Version::new(0, 45, 0), Version::new(1, 0, 0)),
1797 ),
1798 (
1799 "short_name".to_string(),
1800 (Version::new(1, 0, 0), Version::new(2, 0, 0)),
1801 ),
1802 ]
1803 .into_iter()
1804 .collect();
1805
1806 let output = pretty_print_major_versions_available(versions);
1807
1808 insta::assert_snapshot!(output);
1809}
1810
1811#[test]
1812fn test_pretty_print_version_updates() {
1813 let versions = vec![
1814 (
1815 "gleam_stdlib".to_string(),
1816 (Version::new(0, 45, 0), Version::new(0, 46, 0)),
1817 ),
1818 (
1819 "wisp".to_string(),
1820 (Version::new(2, 1, 0), Version::new(2, 1, 1)),
1821 ),
1822 (
1823 "very_long_package_name".to_string(),
1824 (Version::new(12, 12, 12), Version::new(120, 12, 12)),
1825 ),
1826 ]
1827 .into_iter()
1828 .collect();
1829
1830 let output = pretty_print_version_updates(versions);
1831
1832 insta::assert_snapshot!(output);
1833}
1834
1835#[test]
1836fn test_pretty_print_outdated_versions() {
1837 let versions = vec![
1838 (
1839 "gleam_stdlib".to_string(),
1840 (Version::new(0, 45, 0), Version::new(0, 46, 0)),
1841 ),
1842 (
1843 "wisp".to_string(),
1844 (Version::new(2, 1, 0), Version::new(2, 1, 1)),
1845 ),
1846 (
1847 "very_long_package_name".to_string(),
1848 (Version::new(12, 12, 12), Version::new(120, 12, 12)),
1849 ),
1850 ]
1851 .into_iter()
1852 .collect();
1853
1854 let output = pretty_print_outdated_versions(12, versions);
1855
1856 insta::assert_snapshot!(output);
1857}
1858
1859#[test]
1860fn test_pretty_print_outdated_versions_no_updates() {
1861 let versions: HashMap<String, (Version, Version)> = HashMap::new();
1862
1863 let output = pretty_print_outdated_versions(12, versions);
1864
1865 insta::assert_snapshot!(output);
1866}
1867
1868#[test]
1869fn test_ensure_packages_exist_locally_all_present() {
1870 let manifest = Manifest {
1871 requirements: HashMap::new(),
1872 packages: vec![
1873 manifest_package("package_a", "1.0.0", vec![]),
1874 manifest_package("package_b", "2.0.0", vec![]),
1875 manifest_package("package_c", "3.0.0", vec![]),
1876 ],
1877 };
1878
1879 let packages_to_check = vec!["package_a".into(), "package_b".into()];
1880 let result = dependency_manager::ensure_packages_exist_locally(&manifest, &packages_to_check);
1881
1882 assert!(result.is_ok(), "All packages exist, should return Ok");
1883}
1884
1885#[test]
1886fn test_ensure_packages_exist_locally_some_missing() {
1887 let manifest = Manifest {
1888 requirements: HashMap::new(),
1889 packages: vec![
1890 manifest_package("package_a", "1.0.0", vec![]),
1891 manifest_package("package_b", "2.0.0", vec![]),
1892 ],
1893 };
1894
1895 let packages_to_check = vec![
1896 "package_a".into(),
1897 "package_b".into(),
1898 "missing_package".into(),
1899 "another_missing".into(),
1900 ];
1901 let result = dependency_manager::ensure_packages_exist_locally(&manifest, &packages_to_check);
1902
1903 match result {
1904 Err(Error::PackagesToUpdateNotExist { packages }) => {
1905 assert_eq!(packages.len(), 2);
1906 assert!(packages.contains(&"missing_package".into()));
1907 assert!(packages.contains(&"another_missing".into()));
1908 }
1909 _ => panic!("Expected PackagesToUpdateNotExist error"),
1910 }
1911}