1use itertools::Itertools;
2use miette::{Diagnostic, NamedSource, SourceSpan};
3use path_slash::PathBufExt;
4use project_toml::{
5 LocalProjectTomlValidationError, PartialProjectToml, RemoteProjectTomlValidationError,
6};
7use std::{
8 io,
9 ops::Deref,
10 path::{Path, PathBuf},
11 str::FromStr,
12};
13use thiserror::Error;
14use toml_edit::{DocumentMut, Item};
15
16use crate::{
17 config::Config,
18 git::{
19 self,
20 shorthand::RemoteGitUrlShorthand,
21 url::RemoteGitUrl,
22 utils::{GitError, SemVerTagOrSha},
23 },
24 lua_rockspec::{
25 LocalLuaRockspec, LuaRockspecError, LuaVersionError, PartialLuaRockspec,
26 PartialRockspecError, RemoteLuaRockspec,
27 },
28 lua_version::LuaVersion,
29 package::SpecRev,
30 remote_package_db::RemotePackageDB,
31 rockspec::{
32 lua_dependency::{DependencyType, LuaDependencySpec, LuaDependencyType},
33 LuaVersionCompatibility,
34 },
35};
36use crate::{
37 lockfile::PinnedState,
38 package::{PackageName, PackageReq},
39};
40
41pub(crate) mod gen;
42pub mod project_toml;
43
44pub use project_toml::PROJECT_TOML;
45
46pub const EXTRA_ROCKSPEC: &str = "extra.rockspec";
47
48#[derive(Debug, Error, Diagnostic)]
51#[error("{message}", message = _inner.message())]
52#[diagnostic(
53 code(lux_lib::toml::deserialize),
54 help("check that the file is valid TOML and matches the expected schema"),
55 url("https://lux.lumen-labs.org/reference/lux-toml")
56)]
57pub struct TomlDeError {
58 #[source]
59 _inner: Box<toml::de::Error>,
60 #[source_code]
61 src: NamedSource<String>,
62 #[label("here")]
63 span: SourceSpan,
64}
65
66impl TomlDeError {
67 pub fn new(name: &str, src: &str, inner: toml::de::Error) -> Self {
68 let src = NamedSource::new(name, src.to_string());
69 let span = inner.span().unwrap_or(0..0).into();
70 Self {
71 _inner: Box::new(inner),
72 src,
73 span,
74 }
75 }
76
77 pub fn inner(&self) -> &toml::de::Error {
78 &self._inner
79 }
80}
81
82pub(crate) fn parse_toml<T>(name: &str, src: &str) -> Result<T, TomlDeError>
83where
84 T: serde::de::DeserializeOwned,
85{
86 toml::from_str(src).map_err(|e| TomlDeError::new(name, src, e))
87}
88
89#[derive(Error, Debug, Diagnostic)]
90#[non_exhaustive]
91#[error(transparent)]
92pub enum ProjectError {
93 #[error("error reading project TOML at '{toml_path}'")]
94 #[diagnostic(help("ensure the file exists and is readable."))]
95 ReadProjectTOML {
96 toml_path: String,
97 source: io::Error,
98 },
99 #[diagnostic(transparent)]
100 Project(#[from] LocalProjectTomlValidationError),
101 #[diagnostic(transparent)]
102 Toml(#[from] TomlDeError),
103 #[error("error when parsing 'extra.rockspec'")]
104 #[diagnostic(forward(0))]
105 Rockspec(#[from] PartialRockspecError),
106}
107
108#[derive(Error, Debug, Diagnostic)]
109#[non_exhaustive]
110#[error(transparent)]
111pub enum IntoLocalRockspecError {
112 LocalProjectTomlValidationError(Box<LocalProjectTomlValidationError>),
113 RockspecError(Box<LuaRockspecError>),
114}
115
116#[derive(Error, Debug, Diagnostic)]
117#[non_exhaustive]
118#[error(transparent)]
119pub enum IntoRemoteRockspecError {
120 RocksTomlValidationError(Box<RemoteProjectTomlValidationError>),
121 RockspecError(Box<LuaRockspecError>),
122}
123
124#[derive(Error, Debug, Diagnostic)]
125#[non_exhaustive]
126pub enum ProjectEditError {
127 #[error(transparent)]
128 Io(#[from] tokio::io::Error),
129 #[error(transparent)]
130 Toml(#[from] toml_edit::TomlError),
131 #[error("error parsing {PROJECT_TOML} after edit. This is probably a bug.")]
132 #[diagnostic(forward(0))]
133 TomlDe(#[from] TomlDeError),
134 #[error(transparent)]
135 #[diagnostic(transparent)]
136 Git(#[from] GitError),
137 #[error("unable to query latest version for '{0}'")]
138 #[diagnostic(help("check your server config and ensure the package has been published"))]
139 LatestVersionNotFound(PackageName),
140 #[error("expected field to be a value, but got '{0}'")]
141 #[diagnostic(
142 help("ensure your {PROJECT_TOML} matches the specification"),
143 url("https://lux.lumen-labs.org/reference/lux-toml")
144 )]
145 ExpectedValue(Box<toml_edit::Item>),
146 #[error("expected string, but got '{0}'")]
147 #[diagnostic(
148 help("ensure your {PROJECT_TOML} matches the specification"),
149 url("https://lux.lumen-labs.org/reference/lux-toml")
150 )]
151 ExpectedString(Box<toml_edit::Value>),
152 #[error(transparent)]
153 #[diagnostic(transparent)]
154 GitUrlShorthandParse(#[from] git::shorthand::ParseError),
155}
156
157#[derive(Error, Debug, Diagnostic)]
158#[non_exhaustive]
159pub enum PinError {
160 #[error("package '{0}' not found in dependencies")]
161 #[diagnostic(help("to add it, run 'lx add {0}'"))]
162 PackageNotFound(PackageName),
163 #[error("dependency '{dep}' is already {}pinned!", if *.pin_state == PinnedState::Unpinned { "un" } else { "" })]
164 PinStateUnchanged {
165 pin_state: PinnedState,
166 dep: PackageName,
167 },
168 #[error("unable to parse {toml_path} for editing")]
169 #[diagnostic(
170 code(lux_lib::toml_edit::parse),
171 help("check that the file is valid TOML and matches the expected schema"),
172 url("https://lux.lumen-labs.org/reference/lux-toml")
173 )]
174 ParseTomlEdit {
175 toml_path: String,
176 source: toml_edit::TomlError,
177 },
178 #[error("error parsing lux.toml after edit. This is probably a bug.")]
179 #[diagnostic(forward(0))]
180 TomlDe(#[from] TomlDeError),
181 #[error(transparent)]
182 Io(#[from] tokio::io::Error),
183}
184
185#[derive(Clone, Debug)]
188#[cfg_attr(test, derive(Default))]
189pub struct ProjectRoot(PathBuf);
190
191impl ProjectRoot {
192 pub(crate) fn new() -> Self {
193 Self(PathBuf::new())
194 }
195}
196
197impl AsRef<Path> for ProjectRoot {
198 fn as_ref(&self) -> &Path {
199 self.0.as_ref()
200 }
201}
202
203impl Deref for ProjectRoot {
204 type Target = PathBuf;
205
206 fn deref(&self) -> &Self::Target {
207 &self.0
208 }
209}
210
211#[derive(Clone, Debug)]
213pub struct Project {
214 root: ProjectRoot,
216 toml: PartialProjectToml,
218}
219
220impl Project {
221 #[tracing::instrument(level = "trace", skip_all)]
223 pub fn from_exact(start: impl AsRef<Path>) -> Result<Option<Self>, ProjectError> {
224 if !start.as_ref().exists() {
225 return Ok(None);
226 }
227
228 if start.as_ref().join(PROJECT_TOML).exists() {
229 let project_toml_path = start.as_ref().join(PROJECT_TOML);
230 let toml_content = std::fs::read_to_string(&project_toml_path).map_err(|source| {
231 ProjectError::ReadProjectTOML {
232 toml_path: project_toml_path.to_string_lossy().to_string(),
233 source,
234 }
235 })?;
236 let root = start.as_ref();
237
238 let mut project = Project {
239 root: ProjectRoot(root.to_path_buf()),
240 toml: PartialProjectToml::new(
241 &project_toml_path.to_string_lossy(),
242 &toml_content,
243 ProjectRoot(root.to_path_buf()),
244 )?,
245 };
246
247 if let Some(extra_rockspec) = project.extra_rockspec()? {
248 project.toml = project.toml.merge(extra_rockspec);
249 }
250
251 Ok(Some(project))
252 } else {
253 Ok(None)
254 }
255 }
256
257 pub fn toml_path(&self) -> PathBuf {
259 self.root.join(PROJECT_TOML)
260 }
261
262 pub fn extra_rockspec_path(&self) -> PathBuf {
264 self.root.join(EXTRA_ROCKSPEC)
265 }
266
267 pub fn root(&self) -> &ProjectRoot {
268 &self.root
269 }
270
271 pub fn toml(&self) -> &PartialProjectToml {
272 &self.toml
273 }
274
275 pub fn local_rockspec(&self) -> Result<LocalLuaRockspec, IntoLocalRockspecError> {
276 self.toml()
277 .into_local()
278 .map_err(|err| IntoLocalRockspecError::LocalProjectTomlValidationError(Box::new(err)))?
279 .to_lua_rockspec()
280 .map_err(|err| IntoLocalRockspecError::RockspecError(Box::new(err)))
281 }
282
283 pub fn remote_rockspec(
284 &self,
285 specrev: Option<SpecRev>,
286 ) -> Result<RemoteLuaRockspec, IntoRemoteRockspecError> {
287 self.toml()
288 .into_remote(specrev)
289 .map_err(|err| IntoRemoteRockspecError::RocksTomlValidationError(Box::new(err)))?
290 .to_lua_rockspec()
291 .map_err(|err| IntoRemoteRockspecError::RockspecError(Box::new(err)))
292 }
293
294 pub fn extra_rockspec(&self) -> Result<Option<PartialLuaRockspec>, PartialRockspecError> {
295 if self.extra_rockspec_path().exists() {
296 Ok(Some(PartialLuaRockspec::new(&std::fs::read_to_string(
297 self.extra_rockspec_path(),
298 )?)?))
299 } else {
300 Ok(None)
301 }
302 }
303
304 pub fn lua_version(&self, config: &Config) -> Result<LuaVersion, LuaVersionError> {
305 self.toml().lua_version_matches(config)
306 }
307
308 pub async fn add(
309 &mut self,
310 dependencies: DependencyType<&PackageReq>,
311 package_db: &RemotePackageDB,
312 ) -> Result<(), ProjectEditError> {
313 let mut project_toml =
314 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
315
316 prepare_dependency_tables(&mut project_toml);
317 let table = match dependencies {
318 DependencyType::Regular(_) => &mut project_toml["dependencies"],
319 DependencyType::Build(_) => &mut project_toml["build_dependencies"],
320 DependencyType::Test(_) => &mut project_toml["test_dependencies"],
321 DependencyType::External(_) => &mut project_toml["external_dependencies"],
322 };
323
324 match dependencies {
325 DependencyType::Regular(ref deps)
326 | DependencyType::Build(ref deps)
327 | DependencyType::Test(ref deps) => {
328 for dep in deps {
329 let dep_version_str = if dep.version_req().is_any() {
330 package_db
331 .latest_version(dep.name())
332 .map(|latest_version| latest_version.to_string())
333 .unwrap_or_else(|| dep.version_req().to_string())
334 } else {
335 dep.version_req().to_string()
336 };
337 table[dep.name().to_string()] = toml_edit::value(dep_version_str);
338 }
339 }
340 DependencyType::External(ref deps) => {
341 for (name, dep) in deps {
342 if let Some(path) = &dep.header {
343 table[name]["header"] = toml_edit::value(path.to_slash_lossy().to_string());
344 }
345 if let Some(path) = &dep.library {
346 table[name]["library"] =
347 toml_edit::value(path.to_slash_lossy().to_string());
348 }
349 }
350 }
351 };
352
353 let toml_content = project_toml.to_string();
354 tokio::fs::write(self.toml_path(), &toml_content).await?;
355 self.toml = PartialProjectToml::new(
356 self.toml_path().to_str().unwrap_or("<lux.toml>"),
357 &toml_content,
358 self.root.clone(),
359 )?;
360
361 Ok(())
362 }
363
364 pub async fn add_git(
365 &mut self,
366 dependencies: LuaDependencyType<&RemoteGitUrlShorthand>,
367 ) -> Result<(), ProjectEditError> {
368 let mut project_toml =
369 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
370
371 prepare_dependency_tables(&mut project_toml);
372 let table = match dependencies {
373 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
374 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
375 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
376 };
377
378 match dependencies {
379 LuaDependencyType::Regular(urls)
380 | LuaDependencyType::Build(urls)
381 | LuaDependencyType::Test(urls) => {
382 for url in urls {
383 let git_url: RemoteGitUrl = url.clone().into();
384 let mut dep_entry = toml_edit::table();
385 match git::utils::latest_semver_tag_or_commit_sha(&git_url)? {
386 SemVerTagOrSha::SemVerTag(tag) => {
387 dep_entry["git"] = Item::Value(url.to_string().into());
388 dep_entry["version"] = Item::Value(tag.clone().into());
389 if tag.contains("-") {
390 dep_entry["rev"] = Item::Value(tag.into());
392 }
393 }
394 SemVerTagOrSha::CommitSha(sha) => {
395 dep_entry["git"] = Item::Value(url.to_string().into());
396 dep_entry["version"] = Item::Value(sha.into());
397 }
398 }
399 table[git_url.repo()] = dep_entry;
400 }
401 }
402 }
403
404 let toml_content = project_toml.to_string();
405 tokio::fs::write(self.toml_path(), &toml_content).await?;
406 self.toml = PartialProjectToml::new(
407 self.toml_path().to_str().unwrap_or("<lux.toml>"),
408 &toml_content,
409 self.root.clone(),
410 )?;
411
412 Ok(())
413 }
414
415 pub async fn remove(
416 &mut self,
417 dependencies: DependencyType<&PackageName>,
418 ) -> Result<(), ProjectEditError> {
419 let mut project_toml =
420 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
421
422 prepare_dependency_tables(&mut project_toml);
423 let table = match dependencies {
424 DependencyType::Regular(_) => &mut project_toml["dependencies"],
425 DependencyType::Build(_) => &mut project_toml["build_dependencies"],
426 DependencyType::Test(_) => &mut project_toml["test_dependencies"],
427 DependencyType::External(_) => &mut project_toml["external_dependencies"],
428 };
429
430 match dependencies {
431 DependencyType::Regular(ref deps)
432 | DependencyType::Build(ref deps)
433 | DependencyType::Test(ref deps) => {
434 for dep in deps {
435 table[dep.to_string()] = Item::None;
436 }
437 }
438 DependencyType::External(ref deps) => {
439 for (name, dep) in deps {
440 if dep.header.is_some() {
441 table[name]["header"] = Item::None;
442 }
443 if dep.library.is_some() {
444 table[name]["library"] = Item::None;
445 }
446 }
447 }
448 };
449
450 let toml_content = project_toml.to_string();
451 tokio::fs::write(self.toml_path(), &toml_content).await?;
452 self.toml = PartialProjectToml::new(
453 self.toml_path().to_str().unwrap_or("<lux.toml>"),
454 &toml_content,
455 self.root.clone(),
456 )?;
457
458 Ok(())
459 }
460
461 pub async fn upgrade(
462 &mut self,
463 dependencies: LuaDependencyType<&PackageName>,
464 package_db: &RemotePackageDB,
465 ) -> Result<(), ProjectEditError> {
466 let mut project_toml =
467 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
468
469 prepare_dependency_tables(&mut project_toml);
470 let table = match dependencies {
471 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
472 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
473 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
474 };
475
476 match dependencies {
477 LuaDependencyType::Regular(deps)
478 | LuaDependencyType::Build(deps)
479 | LuaDependencyType::Test(deps) => {
480 let latest_rock_version_str =
481 |dep: &PackageName| -> Result<String, ProjectEditError> {
482 Ok(package_db
483 .latest_version(dep)
484 .ok_or(ProjectEditError::LatestVersionNotFound(dep.clone()))?
485 .to_string())
486 };
487 for dep in deps {
488 let mut dep_item = table[dep.to_string()].clone();
489 match &dep_item {
490 Item::Value(_) => {
491 let dep_version_str = latest_rock_version_str(dep)?;
492 table[dep.to_string()] = toml_edit::value(dep_version_str);
493 }
494 Item::Table(tbl) => {
495 match tbl.get("git") {
496 Some(git_item) => {
497 let git_value =
498 git_item.clone().into_value().map_err(|err| {
499 ProjectEditError::ExpectedValue(Box::new(err))
500 })?;
501 let git_url_str = git_value.as_str().ok_or(
502 ProjectEditError::ExpectedString(Box::new(
503 git_value.clone(),
504 )),
505 )?;
506 let shorthand: RemoteGitUrlShorthand = git_url_str.parse()?;
507 match git::utils::latest_semver_tag_or_commit_sha(
508 &shorthand.into(),
509 )? {
510 SemVerTagOrSha::SemVerTag(latest_tag) => {
511 table[dep.to_string()]["version"] =
512 Item::Value(latest_tag.clone().into());
513 if latest_tag.contains("-") {
514 table[dep.to_string()]["rev"] =
516 Item::Value(latest_tag.into());
517 }
518 }
519 SemVerTagOrSha::CommitSha(latest_sha) => {
520 table[dep.to_string()]["version"] =
521 Item::Value(latest_sha.into());
522 }
523 }
524 table[dep.to_string()] = dep_item;
525 }
526 None => {
527 let dep_version_str = latest_rock_version_str(dep)?;
528 dep_item["version".to_string()] =
529 toml_edit::value(dep_version_str);
530 table[dep.to_string()] = dep_item;
531 }
532 }
533 }
534 _ => {}
535 }
536 }
537 }
538 }
539
540 let toml_content = project_toml.to_string();
541 tokio::fs::write(self.toml_path(), &toml_content).await?;
542 self.toml = PartialProjectToml::new(
543 self.toml_path().to_str().unwrap_or("<lux.toml>"),
544 &toml_content,
545 self.root.clone(),
546 )?;
547
548 Ok(())
549 }
550
551 pub async fn upgrade_all(
552 &mut self,
553 package_db: &RemotePackageDB,
554 ) -> Result<(), ProjectEditError> {
555 if let Some(dependencies) = &self.toml().dependencies {
556 let packages = dependencies
557 .iter()
558 .map(|dep| dep.name())
559 .cloned()
560 .collect_vec();
561 self.upgrade(
562 LuaDependencyType::Regular(packages.iter().collect()),
563 package_db,
564 )
565 .await?;
566 }
567 if let Some(dependencies) = &self.toml().build_dependencies {
568 let packages = dependencies
569 .iter()
570 .map(|dep| dep.name())
571 .cloned()
572 .collect_vec();
573 self.upgrade(
574 LuaDependencyType::Build(packages.iter().collect()),
575 package_db,
576 )
577 .await?;
578 }
579 if let Some(dependencies) = &self.toml().test_dependencies {
580 let packages = dependencies
581 .iter()
582 .map(|dep| dep.name())
583 .cloned()
584 .collect_vec();
585 self.upgrade(
586 LuaDependencyType::Test(packages.iter().collect()),
587 package_db,
588 )
589 .await?;
590 }
591 Ok(())
592 }
593
594 pub async fn set_pinned_state(
595 &mut self,
596 dependencies: LuaDependencyType<&PackageName>,
597 pin: PinnedState,
598 ) -> Result<(), PinError> {
599 let mut project_toml =
600 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)
601 .map_err(|source| PinError::ParseTomlEdit {
602 toml_path: self.toml_path().to_slash_lossy().to_string(),
603 source,
604 })?;
605
606 prepare_dependency_tables(&mut project_toml);
607 let table = match dependencies {
608 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
609 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
610 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
611 };
612
613 match dependencies {
614 LuaDependencyType::Regular(ref _deps) => {
615 self.toml.dependencies = Some(
616 self.toml
617 .dependencies
618 .take()
619 .unwrap_or_default()
620 .into_iter()
621 .map(|dep| LuaDependencySpec { pin, ..dep })
622 .collect(),
623 )
624 }
625 LuaDependencyType::Build(ref _deps) => {
626 self.toml.build_dependencies = Some(
627 self.toml
628 .build_dependencies
629 .take()
630 .unwrap_or_default()
631 .into_iter()
632 .map(|dep| LuaDependencySpec { pin, ..dep })
633 .collect(),
634 )
635 }
636 LuaDependencyType::Test(ref _deps) => {
637 self.toml.test_dependencies = Some(
638 self.toml
639 .test_dependencies
640 .take()
641 .unwrap_or_default()
642 .into_iter()
643 .map(|dep| LuaDependencySpec { pin, ..dep })
644 .collect(),
645 )
646 }
647 }
648
649 match dependencies {
650 LuaDependencyType::Regular(ref deps)
651 | LuaDependencyType::Build(ref deps)
652 | LuaDependencyType::Test(ref deps) => {
653 for dep in deps {
654 let mut dep_item = table[dep.to_string()].clone();
655 match dep_item {
656 version @ Item::Value(_) => match &pin {
657 PinnedState::Unpinned => {}
658 PinnedState::Pinned => {
659 if let Ok(mut dep_entry) = toml_edit::table().into_table() {
660 dep_entry.set_implicit(true);
661 dep_entry["version"] = version;
662 dep_entry["pin"] = toml_edit::value(true);
663 table[dep.to_string()] = toml_edit::Item::Table(dep_entry);
664 }
665 }
666 },
667 Item::Table(_) => {
668 dep_item["pin".to_string()] = toml_edit::value(pin.as_bool());
669 table[dep.to_string()] = dep_item;
670 }
671 _ => {}
672 }
673 }
674 }
675 }
676
677 let toml_content = project_toml.to_string();
678 tokio::fs::write(self.toml_path(), &toml_content).await?;
679 self.toml = PartialProjectToml::new(
680 self.toml_path().to_str().unwrap_or("<lux.toml>"),
681 &toml_content,
682 self.root.clone(),
683 )?;
684
685 Ok(())
686 }
687
688 pub fn project_files(&self) -> Vec<PathBuf> {
689 project_files(&self.root().0)
690 }
691}
692
693pub(crate) fn project_files(src: &Path) -> Vec<PathBuf> {
696 ignore::WalkBuilder::new(src)
697 .add(src.join(".cargo"))
698 .follow_links(false)
699 .build()
700 .filter_map(Result::ok)
701 .filter(|entry| entry.file_type().is_some_and(|ft| ft.is_file()))
702 .map(|entry| entry.into_path())
703 .collect_vec()
704}
705
706fn prepare_dependency_tables(project_toml: &mut DocumentMut) {
707 if !project_toml.contains_table("dependencies") {
708 if let Ok(mut table) = toml_edit::table().into_table() {
709 table.set_implicit(true);
710 project_toml["dependencies"] = toml_edit::Item::Table(table);
711 }
712 }
713 if !project_toml.contains_table("build_dependencies") {
714 if let Ok(mut table) = toml_edit::table().into_table() {
715 table.set_implicit(true);
716 project_toml["build_dependencies"] = toml_edit::Item::Table(table);
717 }
718 }
719 if !project_toml.contains_table("test_dependencies") {
720 if let Ok(mut table) = toml_edit::table().into_table() {
721 table.set_implicit(true);
722 project_toml["test_dependencies"] = toml_edit::Item::Table(table);
723 }
724 }
725 if !project_toml.contains_table("external_dependencies") {
726 if let Ok(mut table) = toml_edit::table().into_table() {
727 table.set_implicit(true);
728 project_toml["external_dependencies"] = toml_edit::Item::Table(table);
729 }
730 }
731}
732
733#[cfg(test)]
735mod tests {
736 use std::collections::HashMap;
737
738 use assert_fs::prelude::{PathChild, PathCopy, PathCreateDir};
739 use url::Url;
740
741 use super::*;
742 use crate::{
743 lua_rockspec::ExternalDependencySpec,
744 manifest::{Manifest, ManifestMetadata},
745 package::PackageReq,
746 rockspec::Rockspec,
747 };
748
749 #[tokio::test]
750 async fn test_add_various_dependencies() {
751 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
752 .join("resources/test/sample-projects/no-build-spec/");
753 let project_root = assert_fs::TempDir::new().unwrap();
754 project_root.copy_from(&sample_project, &["**"]).unwrap();
755 let project_root: PathBuf = project_root.path().into();
756 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
757 let add_dependencies = [PackageReq::new("busted".into(), Some(">= 1.0.0".into())).unwrap()];
758 let expected_dependencies = vec![PackageReq::new("busted".into(), Some(">= 1.0.0".into()))
759 .unwrap()
760 .into()];
761
762 let test_manifest_path =
763 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
764 let content = String::from_utf8(std::fs::read(&test_manifest_path).unwrap()).unwrap();
765 let metadata = ManifestMetadata::new(&content).unwrap();
766 let package_db = Manifest::new(Url::parse("https://example.com").unwrap(), metadata).into();
767
768 project
769 .add(
770 DependencyType::Regular(add_dependencies.iter().collect_vec()),
771 &package_db,
772 )
773 .await
774 .unwrap();
775
776 project
777 .add(
778 DependencyType::Build(add_dependencies.iter().collect_vec()),
779 &package_db,
780 )
781 .await
782 .unwrap();
783 project
784 .add(
785 DependencyType::Test(add_dependencies.iter().collect_vec()),
786 &package_db,
787 )
788 .await
789 .unwrap();
790
791 project
792 .add(
793 DependencyType::External(HashMap::from([(
794 "lib".into(),
795 ExternalDependencySpec {
796 library: Some("path.so".into()),
797 header: None,
798 },
799 )])),
800 &package_db,
801 )
802 .await
803 .unwrap();
804
805 let project = Project::from_exact(&project_root).unwrap().unwrap();
808 let validated_toml = project.toml().into_remote(None).unwrap();
809
810 assert_eq!(
811 validated_toml.dependencies().current_platform(),
812 &expected_dependencies
813 );
814 assert_eq!(
815 validated_toml.build_dependencies().current_platform(),
816 &expected_dependencies
817 );
818 assert_eq!(
819 validated_toml.test_dependencies().current_platform(),
820 &expected_dependencies
821 );
822 assert_eq!(
823 validated_toml
824 .external_dependencies()
825 .current_platform()
826 .get("lib")
827 .unwrap(),
828 &ExternalDependencySpec {
829 library: Some("path.so".into()),
830 header: None
831 }
832 );
833 }
834
835 #[tokio::test]
836 async fn test_remove_dependencies() {
837 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
838 .join("resources/test/sample-projects/dependencies/");
839 let project_root = assert_fs::TempDir::new().unwrap();
840 project_root.copy_from(&sample_project, &["**"]).unwrap();
841 let project_root: PathBuf = project_root.path().into();
842 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
843 let lua_cjson = "lua-cjson".into();
844 let plenary_nvim = "plenary.nvim".into();
845 let remove_dependencies = vec![&lua_cjson, &plenary_nvim];
846 project
847 .remove(DependencyType::Regular(remove_dependencies.clone()))
848 .await
849 .unwrap();
850 let check = |project: &Project| {
851 for name in &remove_dependencies {
852 assert!(!project
853 .toml()
854 .dependencies
855 .clone()
856 .unwrap_or_default()
857 .iter()
858 .any(|dep| &dep.name() == name));
859 }
860 };
861 check(&project);
862 let reloaded_project = Project::from_exact(&project_root).unwrap().unwrap();
864 check(&reloaded_project);
865 }
866
867 #[tokio::test]
868 async fn test_extra_rockspec_parsing() {
869 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
870 .join("resources/test/sample-projects/extra-rockspec/");
871 let project_root = assert_fs::TempDir::new().unwrap();
872 project_root.copy_from(&sample_project, &["**"]).unwrap();
873 let project_root: PathBuf = project_root.path().into();
874 let project = Project::from_exact(project_root).unwrap().unwrap();
875
876 let extra_rockspec = project.extra_rockspec().unwrap();
877
878 assert!(extra_rockspec.is_some());
879
880 let rocks = project.toml().into_remote(None).unwrap();
881
882 assert_eq!(rocks.package().to_string(), "custom-package");
883 }
884
885 #[tokio::test]
886 async fn test_pin_dependencies() {
887 test_pin_unpin_dependencies(PinnedState::Pinned).await
888 }
889
890 #[tokio::test]
891 async fn test_unpin_dependencies() {
892 test_pin_unpin_dependencies(PinnedState::Unpinned).await
893 }
894
895 async fn test_pin_unpin_dependencies(pin: PinnedState) {
896 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
897 .join("resources/test/sample-projects/dependencies/");
898 let project_root = assert_fs::TempDir::new().unwrap();
899 project_root.copy_from(&sample_project, &["**"]).unwrap();
900 let project_root: PathBuf = project_root.path().into();
901 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
902 let lua_cjson = "lua-cjson".into();
903 let plenary_nvim = "plenary.nvim".into();
904 let pin_dependencies = vec![&lua_cjson, &plenary_nvim];
905 project
906 .set_pinned_state(LuaDependencyType::Regular(pin_dependencies.clone()), pin)
907 .await
908 .unwrap();
909 let check = |project: &Project| {
910 for name in &pin_dependencies {
911 assert!(project
912 .toml()
913 .dependencies
914 .clone()
915 .unwrap_or_default()
916 .iter()
917 .any(|dep| &dep.name() == name && dep.pin == pin));
918 }
919 };
920 check(&project);
921 let reloaded_project = Project::from_exact(&project_root).unwrap().unwrap();
923 check(&reloaded_project);
924 }
925
926 #[tokio::test]
927 async fn project_files_includes_cargo_directory() {
928 let project_root = assert_fs::TempDir::new().unwrap();
929 let cargo_dir = project_root.child(".cargo");
930 cargo_dir.create_dir_all().unwrap();
931 let cargo_config = cargo_dir.join("config.toml");
932 tokio::fs::write(&cargo_config, "").await.unwrap();
933 let project_files = project_files(&project_root);
934 assert!(project_files.contains(&cargo_config.to_path_buf()));
935 }
936
937 #[tokio::test]
938 async fn from_exact_on_nonexistent_directory() {
939 let empty_dir = assert_fs::TempDir::new().unwrap();
940 assert!(Project::from_exact(&empty_dir).unwrap().is_none());
941 }
942}