Fork of daniellemaywood.uk/gleam — Wasm codegen work
30 kB
856 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2021 The Gleam contributors
3
4use std::collections::HashMap;
5use std::fmt;
6
7use crate::Result;
8use crate::io::{make_relative, ordered_map};
9use crate::requirement::Requirement;
10use camino::{Utf8Path, Utf8PathBuf};
11use ecow::EcoString;
12use hexpm::version::Version;
13use itertools::Itertools;
14
15#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
16pub struct Manifest {
17 #[serde(
18 serialize_with = "ordered_map",
19 deserialize_with = "super::config::map_with_package_name_keys::deserialize"
20 )]
21 pub requirements: HashMap<EcoString, Requirement>,
22 #[serde(serialize_with = "sorted_vec")]
23 pub packages: Vec<ManifestPackage>,
24}
25
26impl Manifest {
27 // Rather than using the toml library to do serialization we implement it
28 // manually so that we can control the formatting.
29 // We want to keep entries on a single line each so that they are more
30 // resistant to merge conflicts and are easier to fix when it does happen.
31 pub fn to_toml(&self, root_path: &Utf8Path) -> String {
32 let mut buffer = String::new();
33 let Self {
34 requirements,
35 packages,
36 } = self;
37
38 buffer.push_str(
39 "# Do not manually edit this file, it is managed by Gleam.
40#
41# This file locks the dependency versions used, to make your build
42# deterministic and to prevent unexpected versions from being included
43# in your application.
44#
45# You should check this file into your source control repository.
46
47",
48 );
49
50 // Packages
51 buffer.push_str("packages = [\n");
52 for ManifestPackage {
53 name,
54 source,
55 version,
56 otp_app,
57 build_tools,
58 requirements,
59 } in packages.iter().sorted_by(|a, b| a.name.cmp(&b.name))
60 {
61 buffer.push_str(r#" {"#);
62 buffer.push_str(r#" name = ""#);
63 buffer.push_str(name);
64 buffer.push_str(r#"", version = ""#);
65 buffer.push_str(&version.to_string());
66 buffer.push_str(r#"", build_tools = ["#);
67 for (i, tool) in build_tools.iter().enumerate() {
68 if i != 0 {
69 buffer.push_str(", ");
70 }
71 buffer.push('"');
72 buffer.push_str(tool);
73 buffer.push('"');
74 }
75
76 buffer.push_str("], requirements = [");
77 for (i, package) in requirements.iter().sorted_by(|a, b| a.cmp(b)).enumerate() {
78 if i != 0 {
79 buffer.push_str(", ");
80 }
81 buffer.push('"');
82 buffer.push_str(package);
83 buffer.push('"');
84 }
85 buffer.push(']');
86
87 if let Some(app) = otp_app {
88 buffer.push_str(", otp_app = \"");
89 buffer.push_str(app);
90 buffer.push('"');
91 }
92
93 match source {
94 ManifestPackageSource::Hex { outer_checksum } => {
95 buffer.push_str(r#", source = "hex", outer_checksum = ""#);
96 buffer.push_str(&outer_checksum.base_16_encoded_string());
97 buffer.push('"');
98 }
99 ManifestPackageSource::Git { repo, commit } => {
100 buffer.push_str(r#", source = "git", repo = ""#);
101 buffer.push_str(repo);
102 buffer.push_str(r#"", commit = ""#);
103 buffer.push_str(commit);
104 buffer.push('"');
105 }
106 ManifestPackageSource::Local { path } => {
107 buffer.push_str(r#", source = "local", path = ""#);
108 buffer.push_str(&make_relative(root_path, path).as_str().replace('\\', "/"));
109 buffer.push('"');
110 }
111 };
112
113 buffer.push_str(" },\n");
114 }
115 buffer.push_str("]\n\n");
116
117 // Requirements
118 buffer.push_str("[requirements]\n");
119 for (name, requirement) in requirements.iter().sorted_by(|a, b| a.0.cmp(b.0)) {
120 buffer.push_str(name);
121 buffer.push_str(" = ");
122 buffer.push_str(&requirement.to_toml(root_path));
123 buffer.push('\n');
124 }
125
126 buffer
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
131pub struct Base16Checksum(pub Vec<u8>);
132
133impl Base16Checksum {
134 pub fn base_16_encoded_string(&self) -> String {
135 base16::encode_upper(&self.0)
136 }
137}
138
139impl serde::Serialize for Base16Checksum {
140 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
141 where
142 S: serde::Serializer,
143 {
144 serializer.serialize_str(&base16::encode_upper(&self.0))
145 }
146}
147
148impl<'de> serde::Deserialize<'de> for Base16Checksum {
149 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
150 where
151 D: serde::Deserializer<'de>,
152 {
153 deserializer.deserialize_str(Base16ChecksumVisitor)
154 }
155}
156
157struct Base16ChecksumVisitor;
158
159impl<'de> serde::de::Visitor<'de> for Base16ChecksumVisitor {
160 type Value = Base16Checksum;
161
162 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
163 formatter.write_str("a base 16 checksum")
164 }
165
166 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
167 where
168 E: serde::de::Error,
169 {
170 base16::decode(value)
171 .map(Base16Checksum)
172 .map_err(serde::de::Error::custom)
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
177pub struct ManifestPackage {
178 #[serde(deserialize_with = "super::config::package_name::deserialize")]
179 pub name: EcoString,
180 pub version: Version,
181 pub build_tools: Vec<EcoString>,
182 #[serde(
183 default,
184 deserialize_with = "super::config::package_name::optional_deserialize"
185 )]
186 pub otp_app: Option<EcoString>,
187 #[serde(serialize_with = "sorted_vec")]
188 pub requirements: Vec<EcoString>,
189 #[serde(flatten)]
190 pub source: ManifestPackageSource,
191}
192
193impl ManifestPackage {
194 pub fn with_build_tools(mut self, build_tools: &'static [&'static str]) -> Self {
195 self.build_tools = build_tools.iter().map(|s| (*s).into()).collect();
196 self
197 }
198
199 pub fn application_name(&self) -> &EcoString {
200 match self.otp_app {
201 Some(ref app) => app,
202 None => &self.name,
203 }
204 }
205
206 #[inline]
207 pub fn is_hex(&self) -> bool {
208 matches!(self.source, ManifestPackageSource::Hex { .. })
209 }
210
211 #[inline]
212 pub fn is_local(&self) -> bool {
213 matches!(self.source, ManifestPackageSource::Local { .. })
214 }
215
216 #[inline]
217 pub fn is_git(&self) -> bool {
218 matches!(self.source, ManifestPackageSource::Git { .. })
219 }
220}
221
222#[cfg(test)]
223impl Default for ManifestPackage {
224 fn default() -> Self {
225 Self {
226 name: Default::default(),
227 build_tools: Default::default(),
228 otp_app: Default::default(),
229 requirements: Default::default(),
230 version: Version::new(1, 0, 0),
231 source: ManifestPackageSource::Hex {
232 outer_checksum: Base16Checksum(vec![]),
233 },
234 }
235 }
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
239#[serde(tag = "source")]
240pub enum ManifestPackageSource {
241 #[serde(rename = "hex")]
242 Hex { outer_checksum: Base16Checksum },
243 #[serde(rename = "git")]
244 Git { repo: EcoString, commit: EcoString },
245 #[serde(rename = "local")]
246 Local { path: Utf8PathBuf }, // should be the canonical path
247}
248
249impl ManifestPackageSource {
250 pub fn kind(&self) -> ManifestPackageSourceKind {
251 match self {
252 ManifestPackageSource::Hex { .. } => ManifestPackageSourceKind::Hex,
253 ManifestPackageSource::Git { .. } => ManifestPackageSourceKind::Git,
254 ManifestPackageSource::Local { .. } => ManifestPackageSourceKind::Local,
255 }
256 }
257}
258
259#[derive(Debug, PartialEq, Eq, Clone, Copy)]
260pub enum ManifestPackageSourceKind {
261 Hex,
262 Git,
263 Local,
264}
265
266fn sorted_vec<S, T>(value: &[T], serializer: S) -> Result<S::Ok, S::Error>
267where
268 S: serde::Serializer,
269 T: serde::Serialize + Ord,
270{
271 use serde::Serialize;
272 let mut value: Vec<&T> = value.iter().collect();
273 value.sort();
274 value.serialize(serializer)
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[cfg(windows)]
282 const HOME: &'static str = "C:\\home\\louis\\packages\\some_folder";
283
284 #[cfg(windows)]
285 const PACKAGE: &'static str = "C:\\home\\louis\\packages\\path\\to\\package";
286
287 #[cfg(windows)]
288 const PACKAGE_WITH_UNC: &'static str = "\\\\?\\C:\\home\\louis\\packages\\path\\to\\package";
289
290 #[cfg(not(windows))]
291 const HOME: &str = "/home/louis/packages/some_folder";
292
293 #[cfg(not(windows))]
294 const PACKAGE: &str = "/home/louis/packages/path/to/package";
295
296 #[test]
297 fn manifest_toml_format() {
298 let manifest = Manifest {
299 requirements: [
300 ("zzz".into(), Requirement::hex("> 0.0.0").unwrap()),
301 ("aaa".into(), Requirement::hex("> 0.0.0").unwrap()),
302 (
303 "awsome_local2".into(),
304 Requirement::git("https://github.com/gleam-lang/gleam.git", "bd9fe02f"),
305 ),
306 (
307 "awsome_local1".into(),
308 Requirement::path("../path/to/package"),
309 ),
310 ("gleam_stdlib".into(), Requirement::hex("~> 0.17").unwrap()),
311 ("gleeunit".into(), Requirement::hex("~> 0.1").unwrap()),
312 ]
313 .into(),
314 packages: vec![
315 ManifestPackage {
316 name: "gleam_stdlib".into(),
317 version: Version::new(0, 17, 1),
318 build_tools: ["gleam".into()].into(),
319 otp_app: None,
320 requirements: vec![],
321 source: ManifestPackageSource::Hex {
322 outer_checksum: Base16Checksum(vec![1, 22]),
323 },
324 },
325 ManifestPackage {
326 name: "aaa".into(),
327 version: Version::new(0, 4, 0),
328 build_tools: ["rebar3".into(), "make".into()].into(),
329 otp_app: Some("aaa_app".into()),
330 requirements: vec!["zzz".into(), "gleam_stdlib".into()],
331 source: ManifestPackageSource::Hex {
332 outer_checksum: Base16Checksum(vec![3, 22]),
333 },
334 },
335 ManifestPackage {
336 name: "zzz".into(),
337 version: Version::new(0, 4, 0),
338 build_tools: ["mix".into()].into(),
339 otp_app: None,
340 requirements: vec![],
341 source: ManifestPackageSource::Hex {
342 outer_checksum: Base16Checksum(vec![3, 22]),
343 },
344 },
345 ManifestPackage {
346 name: "awsome_local2".into(),
347 version: Version::new(1, 2, 3),
348 build_tools: ["gleam".into()].into(),
349 otp_app: None,
350 requirements: vec![],
351 source: ManifestPackageSource::Git {
352 repo: "https://github.com/gleam-lang/gleam.git".into(),
353 commit: "bd9fe02f72250e6a136967917bcb1bdccaffa3c8".into(),
354 },
355 },
356 ManifestPackage {
357 name: "awsome_local1".into(),
358 version: Version::new(1, 2, 3),
359 build_tools: ["gleam".into()].into(),
360 otp_app: None,
361 requirements: vec![],
362 source: ManifestPackageSource::Local {
363 path: PACKAGE.into(),
364 },
365 },
366 ManifestPackage {
367 name: "gleeunit".into(),
368 version: Version::new(0, 4, 0),
369 build_tools: ["gleam".into()].into(),
370 otp_app: None,
371 requirements: vec!["gleam_stdlib".into()],
372 source: ManifestPackageSource::Hex {
373 outer_checksum: Base16Checksum(vec![3, 46]),
374 },
375 },
376 ],
377 };
378
379 let buffer = manifest.to_toml(HOME.into());
380 assert_eq!(
381 buffer,
382 r#"# Do not manually edit this file, it is managed by Gleam.
383#
384# This file locks the dependency versions used, to make your build
385# deterministic and to prevent unexpected versions from being included
386# in your application.
387#
388# You should check this file into your source control repository.
389
390packages = [
391 { name = "aaa", version = "0.4.0", build_tools = ["rebar3", "make"], requirements = ["gleam_stdlib", "zzz"], otp_app = "aaa_app", source = "hex", outer_checksum = "0316" },
392 { name = "awsome_local1", version = "1.2.3", build_tools = ["gleam"], requirements = [], source = "local", path = "../path/to/package" },
393 { name = "awsome_local2", version = "1.2.3", build_tools = ["gleam"], requirements = [], source = "git", repo = "https://github.com/gleam-lang/gleam.git", commit = "bd9fe02f72250e6a136967917bcb1bdccaffa3c8" },
394 { name = "gleam_stdlib", version = "0.17.1", build_tools = ["gleam"], requirements = [], source = "hex", outer_checksum = "0116" },
395 { name = "gleeunit", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "hex", outer_checksum = "032E" },
396 { name = "zzz", version = "0.4.0", build_tools = ["mix"], requirements = [], source = "hex", outer_checksum = "0316" },
397]
398
399[requirements]
400aaa = { version = "> 0.0.0" }
401awsome_local1 = { path = "../path/to/package" }
402awsome_local2 = { git = "https://github.com/gleam-lang/gleam.git", ref = "bd9fe02f" }
403gleam_stdlib = { version = "~> 0.17" }
404gleeunit = { version = "~> 0.1" }
405zzz = { version = "> 0.0.0" }
406"#
407 );
408 }
409
410 #[cfg(windows)]
411 #[test]
412 fn manifest_toml_format_with_unc() {
413 let manifest = Manifest {
414 requirements: [
415 ("zzz".into(), Requirement::hex("> 0.0.0").unwrap()),
416 ("aaa".into(), Requirement::hex("> 0.0.0").unwrap()),
417 (
418 "awsome_local2".into(),
419 Requirement::git("https://github.com/gleam-lang/gleam.git", "main"),
420 ),
421 (
422 "awsome_local1".into(),
423 Requirement::path("../path/to/package"),
424 ),
425 ("gleam_stdlib".into(), Requirement::hex("~> 0.17").unwrap()),
426 ("gleeunit".into(), Requirement::hex("~> 0.1").unwrap()),
427 ]
428 .into(),
429 packages: vec![
430 ManifestPackage {
431 name: "gleam_stdlib".into(),
432 version: Version::new(0, 17, 1),
433 build_tools: ["gleam".into()].into(),
434 otp_app: None,
435 requirements: vec![],
436 source: ManifestPackageSource::Hex {
437 outer_checksum: Base16Checksum(vec![1, 22]),
438 },
439 },
440 ManifestPackage {
441 name: "aaa".into(),
442 version: Version::new(0, 4, 0),
443 build_tools: ["rebar3".into(), "make".into()].into(),
444 otp_app: Some("aaa_app".into()),
445 requirements: vec!["zzz".into(), "gleam_stdlib".into()],
446 source: ManifestPackageSource::Hex {
447 outer_checksum: Base16Checksum(vec![3, 22]),
448 },
449 },
450 ManifestPackage {
451 name: "zzz".into(),
452 version: Version::new(0, 4, 0),
453 build_tools: ["mix".into()].into(),
454 otp_app: None,
455 requirements: vec![],
456 source: ManifestPackageSource::Hex {
457 outer_checksum: Base16Checksum(vec![3, 22]),
458 },
459 },
460 ManifestPackage {
461 name: "awsome_local2".into(),
462 version: Version::new(1, 2, 3),
463 build_tools: ["gleam".into()].into(),
464 otp_app: None,
465 requirements: vec![],
466 source: ManifestPackageSource::Git {
467 repo: "https://github.com/gleam-lang/gleam.git".into(),
468 commit: "bd9fe02f72250e6a136967917bcb1bdccaffa3c8".into(),
469 },
470 },
471 ManifestPackage {
472 name: "awsome_local1".into(),
473 version: Version::new(1, 2, 3),
474 build_tools: ["gleam".into()].into(),
475 otp_app: None,
476 requirements: vec![],
477 source: ManifestPackageSource::Local {
478 path: PACKAGE_WITH_UNC.into(),
479 },
480 },
481 ManifestPackage {
482 name: "gleeunit".into(),
483 version: Version::new(0, 4, 0),
484 build_tools: ["gleam".into()].into(),
485 otp_app: None,
486 requirements: vec!["gleam_stdlib".into()],
487 source: ManifestPackageSource::Hex {
488 outer_checksum: Base16Checksum(vec![3, 46]),
489 },
490 },
491 ],
492 };
493
494 let buffer = manifest.to_toml(HOME.into());
495 assert_eq!(
496 buffer,
497 r#"# Do not manually edit this file, it is managed by Gleam.
498#
499# This file locks the dependency versions used, to make your build
500# deterministic and to prevent unexpected versions from being included
501# in your application.
502#
503# You should check this file into your source control repository.
504
505packages = [
506 { name = "aaa", version = "0.4.0", build_tools = ["rebar3", "make"], requirements = ["gleam_stdlib", "zzz"], otp_app = "aaa_app", source = "hex", outer_checksum = "0316" },
507 { name = "awsome_local1", version = "1.2.3", build_tools = ["gleam"], requirements = [], source = "local", path = "../path/to/package" },
508 { name = "awsome_local2", version = "1.2.3", build_tools = ["gleam"], requirements = [], source = "git", repo = "https://github.com/gleam-lang/gleam.git", commit = "bd9fe02f72250e6a136967917bcb1bdccaffa3c8" },
509 { name = "gleam_stdlib", version = "0.17.1", build_tools = ["gleam"], requirements = [], source = "hex", outer_checksum = "0116" },
510 { name = "gleeunit", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "hex", outer_checksum = "032E" },
511 { name = "zzz", version = "0.4.0", build_tools = ["mix"], requirements = [], source = "hex", outer_checksum = "0316" },
512]
513
514[requirements]
515aaa = { version = "> 0.0.0" }
516awsome_local1 = { path = "../path/to/package" }
517awsome_local2 = { git = "https://github.com/gleam-lang/gleam.git", ref = "main" }
518gleam_stdlib = { version = "~> 0.17" }
519gleeunit = { version = "~> 0.1" }
520zzz = { version = "> 0.0.0" }
521"#
522 );
523 }
524}
525
526#[derive(Debug)]
527pub struct Resolved {
528 pub manifest: Manifest,
529 pub package_changes: PackageChanges,
530 pub requirements_changed: bool,
531}
532
533#[derive(Debug)]
534pub struct PackageChanges {
535 pub added: Vec<(EcoString, Version)>,
536 pub changed: Vec<Changed>,
537 /// When updating git dependencies, it is possible to update to a newer commit
538 /// without updating the version of the package (which is specified in
539 /// `gleam.toml`). In this case, we still want to record the change, but it
540 /// must be stored differently.
541 pub changed_git: Vec<ChangedGit>,
542 pub removed: Vec<EcoString>,
543}
544
545#[derive(Debug, PartialEq)]
546pub struct Changed {
547 pub name: EcoString,
548 pub old: Version,
549 pub new: Version,
550}
551
552#[derive(Debug, PartialEq)]
553pub struct ChangedGit {
554 pub name: EcoString,
555 pub old_hash: EcoString,
556 pub new_hash: EcoString,
557}
558
559impl Resolved {
560 pub fn any_changes(&self) -> bool {
561 self.requirements_changed || self.package_changes.any_changes()
562 }
563
564 pub fn all_added(manifest: Manifest) -> Resolved {
565 let added = manifest
566 .packages
567 .iter()
568 .map(|package| (package.name.clone(), package.version.clone()))
569 .collect();
570 Self {
571 manifest,
572 requirements_changed: true,
573 package_changes: PackageChanges {
574 added,
575 changed: vec![],
576 changed_git: vec![],
577 removed: vec![],
578 },
579 }
580 }
581
582 pub fn no_change(manifest: Manifest) -> Self {
583 Self {
584 manifest,
585 requirements_changed: false,
586 package_changes: PackageChanges {
587 added: vec![],
588 changed: vec![],
589 changed_git: vec![],
590 removed: vec![],
591 },
592 }
593 }
594}
595
596impl PackageChanges {
597 pub fn any_changes(&self) -> bool {
598 !self.added.is_empty()
599 || !self.changed.is_empty()
600 || !self.changed_git.is_empty()
601 || !self.removed.is_empty()
602 }
603
604 /// Compare the old and new versions of the manifest and determine the package changes
605 pub fn between_manifests(old: &Manifest, new: &Manifest) -> Self {
606 let mut added = vec![];
607 let mut changed = vec![];
608 let mut changed_git = vec![];
609 let mut removed = vec![];
610 let mut old: HashMap<_, _> = old
611 .packages
612 .iter()
613 .map(|package| (&package.name, package))
614 .collect();
615
616 for new in &new.packages {
617 match old.remove(&new.name) {
618 // If the kind of source changed, the packages bear essentially no connection
619 Some(old) if new.source.kind() != old.source.kind() => {
620 removed.push(old.name.clone());
621 added.push((new.name.clone(), new.version.clone()));
622 }
623 Some(old) if old.version == new.version => match (&old.source, &new.source) {
624 (
625 ManifestPackageSource::Git {
626 commit: old_hash, ..
627 },
628 ManifestPackageSource::Git {
629 commit: new_hash, ..
630 },
631 ) if old_hash != new_hash => changed_git.push(ChangedGit {
632 name: new.name.clone(),
633 old_hash: old_hash.clone(),
634 new_hash: new_hash.clone(),
635 }),
636 (
637 ManifestPackageSource::Hex { .. }
638 | ManifestPackageSource::Local { .. }
639 | ManifestPackageSource::Git { .. },
640 _,
641 ) => {}
642 },
643 Some(old) => {
644 changed.push(Changed {
645 name: new.name.clone(),
646 old: old.version.clone(),
647 new: new.version.clone(),
648 });
649 }
650 None => {
651 added.push((new.name.clone(), new.version.clone()));
652 }
653 }
654 }
655
656 removed.extend(old.into_keys().cloned());
657
658 Self {
659 added,
660 changed,
661 changed_git,
662 removed,
663 }
664 }
665}
666
667#[cfg(test)]
668mod manifest_update_tests {
669 use std::collections::HashMap;
670
671 use ecow::EcoString;
672 use hexpm::version::Version;
673
674 use crate::manifest::{Base16Checksum, ManifestPackage, ManifestPackageSource, PackageChanges};
675 use crate::manifest::{Changed, Manifest};
676
677 #[test]
678 fn resolved_with_updated() {
679 let package = |name: &str, version| ManifestPackage {
680 name: EcoString::from(name),
681 version,
682 build_tools: vec![],
683 otp_app: None,
684 requirements: vec![],
685 source: ManifestPackageSource::Hex {
686 outer_checksum: Base16Checksum(vec![]),
687 },
688 };
689
690 let old = Manifest {
691 requirements: HashMap::new(),
692 packages: vec![
693 package("unchanged1", Version::new(3, 0, 0)),
694 package("unchanged2", Version::new(0, 1, 0)),
695 package("changed1", Version::new(3, 0, 0)),
696 package("changed2", Version::new(0, 1, 0)),
697 package("removed1", Version::new(10, 0, 0)),
698 package("removed2", Version::new(20, 1, 0)),
699 ],
700 };
701
702 let new = Manifest {
703 requirements: HashMap::new(),
704 packages: vec![
705 package("new1", Version::new(1, 0, 0)),
706 package("new2", Version::new(2, 1, 0)),
707 package("unchanged1", Version::new(3, 0, 0)),
708 package("unchanged2", Version::new(0, 1, 0)),
709 package("changed1", Version::new(5, 0, 0)),
710 package("changed2", Version::new(3, 0, 0)),
711 ],
712 };
713
714 let mut changes = PackageChanges::between_manifests(&old, &new);
715 changes.added.sort();
716 changes.changed.sort_by(|a, b| a.name.cmp(&b.name));
717 changes.removed.sort();
718
719 assert_eq!(
720 changes.added,
721 vec![
722 ("new1".into(), Version::new(1, 0, 0)),
723 ("new2".into(), Version::new(2, 1, 0)),
724 ]
725 );
726
727 assert_eq!(
728 changes.changed,
729 vec![
730 Changed {
731 name: "changed1".into(),
732 old: Version::new(3, 0, 0),
733 new: Version::new(5, 0, 0)
734 },
735 Changed {
736 name: "changed2".into(),
737 old: Version::new(0, 1, 0),
738 new: Version::new(3, 0, 0)
739 },
740 ]
741 );
742
743 assert_eq!(
744 changes.removed,
745 vec![EcoString::from("removed1"), EcoString::from("removed2")]
746 );
747 }
748
749 #[test]
750 fn resolved_with_source_type_change() {
751 let name = EcoString::from("wibble");
752 let version = Version::new(1, 0, 0);
753 let package = |source| ManifestPackage {
754 name: name.clone(),
755 version: version.clone(),
756 build_tools: vec![],
757 otp_app: None,
758 requirements: vec![],
759 source,
760 };
761
762 let old = Manifest {
763 requirements: HashMap::new(),
764 packages: vec![package(ManifestPackageSource::Local {
765 path: "wibble".into(),
766 })],
767 };
768
769 let new = Manifest {
770 requirements: HashMap::new(),
771 packages: vec![package(ManifestPackageSource::Hex {
772 outer_checksum: Base16Checksum(vec![]),
773 })],
774 };
775
776 let changes = PackageChanges::between_manifests(&old, &new);
777 assert!(changes.changed.is_empty());
778 assert_eq!(changes.removed, vec![name.clone()]);
779 assert_eq!(changes.added, vec![(name.clone(), version.clone())]);
780 }
781}
782
783#[test]
784fn invalid_requirements_dependency_name() {
785 // This is valid toml but wibble/wobble is not a valid package name
786 let toml = r#"# This file was generated by Gleam
787# You typically do not need to edit this file
788
789packages = [
790 { name = "gleam_stdlib", version = "0.58.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "091F2D2C4A3A4E2047986C47E2C2C9D728A4E068ABB31FDA17B0D347E6248467" },
791]
792
793[requirements]
794"wibble/wobble" = { version = ">= 0.58.0 and < 2.0.0" }
795"#;
796
797 let manifest: Result<Manifest, _> = toml::from_str(toml);
798 let error = manifest.expect_err("should fail to deserialise because invalid name");
799 insta::assert_snapshot!(insta::internals::AutoName, error.to_string());
800}
801
802#[test]
803fn invalid_packages_dependency_name() {
804 // This is valid toml but wibble/wobble is not a valid package name
805 let toml = r#"# This file was generated by Gleam
806# You typically do not need to edit this file
807
808packages = [
809 { name = "wibble/wobble", version = "0.58.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "091F2D2C4A3A4E2047986C47E2C2C9D728A4E068ABB31FDA17B0D347E6248467" },
810]
811
812[requirements]
813gleam_stdlib = { version = ">= 0.58.0 and < 2.0.0" }
814"#;
815
816 let manifest: Result<Manifest, _> = toml::from_str(toml);
817 let error = manifest.expect_err("should fail to deserialise because invalid name");
818 insta::assert_snapshot!(insta::internals::AutoName, error.to_string());
819}
820
821#[test]
822fn invalid_packages_otp_app_name() {
823 // This is valid toml but wibble/wobble is not a valid package name
824 let toml = r#"# This file was generated by Gleam
825# You typically do not need to edit this file
826
827packages = [
828 { name = "gleam_stdlib", version = "0.58.0", build_tools = ["gleam"], requirements = [], otp_app = "wibble/wobble", source = "hex", outer_checksum = "091F2D2C4A3A4E2047986C47E2C2C9D728A4E068ABB31FDA17B0D347E6248467" },
829]
830
831[requirements]
832gleam_stdlib = { version = ">= 0.58.0 and < 2.0.0" }
833"#;
834
835 let manifest: Result<Manifest, _> = toml::from_str(toml);
836 let error = manifest.expect_err("should fail to deserialise because invalid name");
837 insta::assert_snapshot!(insta::internals::AutoName, error.to_string());
838}
839
840#[test]
841fn no_otp_app() {
842 // This is valid toml but wibble/wobble is not a valid package name
843 let toml = r#"# This file was generated by Gleam
844# You typically do not need to edit this file
845
846packages = [
847 { name = "gleam_stdlib", version = "0.58.0", build_tools = ["gleam"], requirements = [], source = "hex", outer_checksum = "091F2D2C4A3A4E2047986C47E2C2C9D728A4E068ABB31FDA17B0D347E6248467" },
848]
849
850[requirements]
851gleam_stdlib = { version = ">= 0.58.0 and < 2.0.0" }
852"#;
853
854 let manifest: Result<Manifest, _> = toml::from_str(toml);
855 assert!(manifest.is_ok());
856}