Skip to main content

lux_lib/project/
mod.rs

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/// A newtype for the project root directory.
112/// This is used to ensure that the project root is a valid project directory.
113#[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/// Lux project, with methods for managing dependencies, etc.
138#[derive(Clone, Debug)]
139pub struct Project {
140    /// The path where the `lux.toml` resides.
141    root: ProjectRoot,
142    /// The parsed lux.toml.
143    toml: PartialProjectToml,
144}
145
146impl Project {
147    /// Load a project at the specified path, if it exists
148    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    /// Get the `lux.toml` path.
176    pub fn toml_path(&self) -> PathBuf {
177        self.root.join(PROJECT_TOML)
178    }
179
180    /// Get the `extra.rockspec` path.
181    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                                // Tag contains a specrev.
297                                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                                                // Tag contains a specrev.
413                                                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
579/// Get the files that Lux treats as project files
580/// This respects ignore files and excludes hidden files and directories.
581pub(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// TODO: More project-based test
620#[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        // Reparse the lux.toml (not usually necessary, but we want to test that the file was
691        // written correctly)
692        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        // check again after reloading lux.toml
747        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        // check again after reloading lux.toml
804        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}