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