Skip to main content

lux_lib/project/
mod.rs

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