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