diff --git a/test/run.hml b/test/run.hml index c02834a..b12bba5 100644 --- a/test/run.hml +++ b/test/run.hml @@ -9,6 +9,9 @@ import * as test_semver from "./test_semver.hml"; import * as test_manifest from "./test_manifest.hml"; import * as test_lockfile from "./test_lockfile.hml"; import * as test_cache from "./test_cache.hml"; +import * as test_resolver from "./test_resolver.hml"; +import * as test_github from "./test_github.hml"; +import * as test_installer from "./test_installer.hml"; print("hpm Test Suite"); print("=============="); @@ -18,6 +21,9 @@ test_semver.run(); test_manifest.run(); test_lockfile.run(); test_cache.run(); +test_resolver.run(); +test_github.run(); +test_installer.run(); // Print summary and exit with appropriate code let exit_code = summary(); diff --git a/test/test_cache.hml b/test/test_cache.hml index 31a91bb..41ebbea 100644 --- a/test/test_cache.hml +++ b/test/test_cache.hml @@ -1,9 +1,33 @@ // Tests for cache module -import { suite, test, assert, assert_eq, assert_true, assert_false, assert_not_null } from "./framework.hml"; -import { getenv } from "@stdlib/env"; +import { suite, test, assert, assert_eq, assert_true, assert_false, assert_null, assert_not_null } from "./framework.hml"; +import { getenv, setenv } from "@stdlib/env"; +import { exists, make_dir, remove_dir, write_file, remove_file, is_dir, list_dir } from "@stdlib/fs"; import * as cache from "../src/cache.hml"; +// Helper to clean up test directories +fn cleanup_dir(path: string) { + if (!exists(path)) { + return; + } + if (!is_dir(path)) { + remove_file(path); + return; + } + let entries = list_dir(path); + let i = 0; + while (i < entries.length) { + let entry_path = path + "/" + entries[i]; + if (is_dir(entry_path)) { + cleanup_dir(entry_path); + } else { + remove_file(entry_path); + } + i = i + 1; + } + remove_dir(path); +} + export fn run() { suite("cache.get_cache_dir"); @@ -21,6 +45,16 @@ export fn run() { } }); + test("cache dir ends with cache", fn() { + let dir = cache.get_cache_dir(); + assert_true(dir.ends_with("/cache"), "should end with /cache"); + }); + + test("cache dir is under .hpm", fn() { + let dir = cache.get_cache_dir(); + assert_true(dir.contains("/.hpm/"), "should be under .hpm"); + }); + suite("cache.get_config_dir"); test("returns path", fn() { @@ -29,6 +63,14 @@ export fn run() { assert_true(dir.ends_with(".hpm"), "should end with .hpm"); }); + test("config dir respects HOME", fn() { + let home = getenv("HOME"); + if (home != null) { + let dir = cache.get_config_dir(); + assert_true(dir.starts_with(home), "should be under HOME"); + } + }); + suite("cache.get_cached_tarball_path"); test("builds correct path", fn() { @@ -45,12 +87,58 @@ export fn run() { assert_true(path.contains("1.0.0"), "version without v"); }); + test("handles complex version", fn() { + let path = cache.get_cached_tarball_path("owner", "repo", "v2.1.3-beta.1"); + assert_true(path.contains("2.1.3-beta.1"), "complex version"); + }); + + test("handles owner with hyphen", fn() { + let path = cache.get_cached_tarball_path("my-org", "repo", "1.0.0"); + assert_true(path.contains("my-org"), "hyphenated owner"); + }); + + test("handles repo with hyphen", fn() { + let path = cache.get_cached_tarball_path("owner", "my-repo", "1.0.0"); + assert_true(path.contains("my-repo"), "hyphenated repo"); + }); + + test("path structure is owner/repo/version.tar.gz", fn() { + let path = cache.get_cached_tarball_path("testowner", "testrepo", "1.2.3"); + assert_true(path.contains("/testowner/testrepo/1.2.3.tar.gz"), "correct structure"); + }); + + suite("cache.get_cached_path (alias)"); + + test("returns same as get_cached_tarball_path", fn() { + let path1 = cache.get_cached_tarball_path("owner", "repo", "1.0.0"); + let path2 = cache.get_cached_path("owner", "repo", "1.0.0"); + assert_eq(path1, path2, "paths should match"); + }); + + suite("cache.is_cached"); + + test("returns false for non-cached package", fn() { + let cached = cache.is_cached("nonexistent", "fakerepo", "9.9.9"); + assert_false(cached, "should not be cached"); + }); + + test("returns false for non-cached version", fn() { + // A package that doesn't exist + let cached = cache.is_cached("testowner", "testrepo", "0.0.0-nonexistent"); + assert_false(cached, "non-existent version not cached"); + }); + suite("cache.list_cached"); test("returns array", fn() { let packages = cache.list_cached(); assert_not_null(packages, "should return array"); - // Note: actual content depends on cache state + }); + + test("returns array with correct structure", fn() { + let packages = cache.list_cached(); + // Even if empty, should be an array + assert_true(packages.length >= 0, "has length property"); }); suite("cache.get_cache_size"); @@ -59,4 +147,80 @@ export fn run() { let size = cache.get_cache_size(); assert_true(size >= 0, "size >= 0"); }); + + test("returns i64 type for large caches", fn() { + let size = cache.get_cache_size(); + // Just verify we can compare it + assert_true(size >= 0, "can compare size"); + }); + + suite("cache.read_config"); + + test("returns object", fn() { + let config = cache.read_config(); + assert_not_null(config, "should return config object"); + }); + + test("returns empty object if no config", fn() { + // Default behavior when no config exists + let config = cache.read_config(); + // Should at least return an object (could be empty) + assert_not_null(config, "config not null"); + }); + + suite("cache.ensure_cache_dir"); + + test("returns cache directory path", fn() { + let dir = cache.ensure_cache_dir(); + assert_not_null(dir, "should return path"); + assert_true(dir.length > 0, "path not empty"); + }); + + test("creates directory if not exists", fn() { + let dir = cache.ensure_cache_dir(); + assert_true(exists(dir), "directory should exist"); + }); + + suite("cache path edge cases"); + + test("handles empty version after v strip", fn() { + // Edge case: what if version is just "v"? + let path = cache.get_cached_tarball_path("owner", "repo", "v"); + assert_true(path.ends_with(".tar.gz"), "still ends with .tar.gz"); + }); + + test("handles version with multiple dots", fn() { + let path = cache.get_cached_tarball_path("owner", "repo", "1.2.3.4.5"); + assert_true(path.contains("1.2.3.4.5"), "multi-dot version"); + }); + + test("handles version with plus sign", fn() { + let path = cache.get_cached_tarball_path("owner", "repo", "1.0.0+build.123"); + assert_true(path.contains("1.0.0+build.123"), "version with build metadata"); + }); + + suite("cache operations"); + + test("store and retrieve tarball file", fn() { + let test_cache = "/tmp/hpm_test_cache"; + cleanup_dir(test_cache); + + // Create a test tarball + let test_tarball = "/tmp/hpm_test_tarball.tar.gz"; + write_file(test_tarball, "fake tarball content for testing"); + + // Store in cache (this creates the directory structure) + make_dir(test_cache); + make_dir(test_cache + "/testowner"); + make_dir(test_cache + "/testowner/testrepo"); + let dest_path = test_cache + "/testowner/testrepo/1.0.0.tar.gz"; + write_file(dest_path, "fake tarball content for testing"); + + // Verify the file exists + assert_true(exists(dest_path), "tarball stored"); + + // Clean up + cleanup_dir(test_cache); + remove_file(test_tarball); + }); } diff --git a/test/test_github.hml b/test/test_github.hml new file mode 100644 index 0000000..14cb95a --- /dev/null +++ b/test/test_github.hml @@ -0,0 +1,108 @@ +// Tests for github module (non-network functions) + +import { suite, test, assert, assert_eq, assert_true, assert_false, assert_null, assert_not_null } from "./framework.hml"; +import * as github from "../src/github.hml"; + +export fn run() { + suite("github.get_tarball_url"); + + test("builds correct URL with v prefix", fn() { + let url = github.get_tarball_url("hemlang", "sprout", "v1.0.0"); + assert_eq(url, "https://github.com/hemlang/sprout/archive/refs/tags/v1.0.0.tar.gz", "url with v"); + }); + + test("adds v prefix if missing", fn() { + let url = github.get_tarball_url("hemlang", "sprout", "1.0.0"); + assert_eq(url, "https://github.com/hemlang/sprout/archive/refs/tags/v1.0.0.tar.gz", "url without v"); + }); + + test("handles complex version strings", fn() { + let url = github.get_tarball_url("owner", "repo", "v2.1.3-beta.1"); + assert_eq(url, "https://github.com/owner/repo/archive/refs/tags/v2.1.3-beta.1.tar.gz", "complex version"); + }); + + test("handles different owners and repos", fn() { + let url = github.get_tarball_url("my-org", "my-package", "v0.0.1"); + assert_eq(url, "https://github.com/my-org/my-package/archive/refs/tags/v0.0.1.tar.gz", "custom org/repo"); + }); + + test("handles single digit version", fn() { + let url = github.get_tarball_url("test", "pkg", "v1"); + assert_eq(url, "https://github.com/test/pkg/archive/refs/tags/v1.tar.gz", "single digit"); + }); + + suite("github.get_token (environment/config)"); + + test("returns null when no token configured", fn() { + // Note: This test assumes no GITHUB_TOKEN is set in the test environment + // and no config file exists. It tests the fallback behavior. + // In a real environment, this might return an actual token. + let token = github.get_token(); + // We can't assert null because there might be a token configured + // Just verify it doesn't crash + assert_true(token == null || token.length >= 0, "token is null or string"); + }); + + suite("github base64 decoding (internal)"); + + // Test base64 decoding by encoding and checking known values + // The base64_decode function is internal but we can test it indirectly + // by testing get_package_json with mock responses + + test("tarball URL is HTTPS", fn() { + let url = github.get_tarball_url("any", "repo", "v1.0.0"); + assert_true(url.starts_with("https://"), "should use HTTPS"); + }); + + test("tarball URL contains .tar.gz extension", fn() { + let url = github.get_tarball_url("owner", "repo", "v1.0.0"); + assert_true(url.ends_with(".tar.gz"), "should end with .tar.gz"); + }); + + test("tarball URL contains owner", fn() { + let url = github.get_tarball_url("myowner", "myrepo", "v1.0.0"); + assert_true(url.contains("/myowner/"), "should contain owner"); + }); + + test("tarball URL contains repo", fn() { + let url = github.get_tarball_url("myowner", "myrepo", "v1.0.0"); + assert_true(url.contains("/myrepo/"), "should contain repo"); + }); + + test("tarball URL uses archive path", fn() { + let url = github.get_tarball_url("owner", "repo", "v1.0.0"); + assert_true(url.contains("/archive/refs/tags/"), "should use archive path"); + }); + + suite("github URL edge cases"); + + test("handles owner with hyphen", fn() { + let url = github.get_tarball_url("my-org-name", "repo", "v1.0.0"); + assert_true(url.contains("/my-org-name/"), "hyphenated owner"); + }); + + test("handles repo with hyphen", fn() { + let url = github.get_tarball_url("owner", "my-cool-repo", "v1.0.0"); + assert_true(url.contains("/my-cool-repo/"), "hyphenated repo"); + }); + + test("handles owner with underscore", fn() { + let url = github.get_tarball_url("my_org", "repo", "v1.0.0"); + assert_true(url.contains("/my_org/"), "underscored owner"); + }); + + test("handles repo with numbers", fn() { + let url = github.get_tarball_url("owner", "repo123", "v1.0.0"); + assert_true(url.contains("/repo123/"), "repo with numbers"); + }); + + test("handles prerelease version", fn() { + let url = github.get_tarball_url("owner", "repo", "v1.0.0-alpha.1"); + assert_true(url.contains("v1.0.0-alpha.1"), "prerelease in URL"); + }); + + test("handles build metadata in version", fn() { + let url = github.get_tarball_url("owner", "repo", "v1.0.0+build.123"); + assert_true(url.contains("v1.0.0+build.123"), "build metadata in URL"); + }); +} diff --git a/test/test_installer.hml b/test/test_installer.hml new file mode 100644 index 0000000..32cd9ca --- /dev/null +++ b/test/test_installer.hml @@ -0,0 +1,276 @@ +// Tests for installer module + +import { suite, test, assert, assert_eq, assert_true, assert_false, assert_null, assert_not_null } from "./framework.hml"; +import { exists, make_dir, remove_dir, write_file, is_dir, list_dir } from "@stdlib/fs"; +import * as installer from "../src/installer.hml"; + +// Helper to remove directory recursively +fn cleanup_dir(path: string) { + if (!exists(path)) { + return; + } + if (!is_dir(path)) { + return; + } + let entries = list_dir(path); + let i = 0; + while (i < entries.length) { + let entry_path = path + "/" + entries[i]; + if (is_dir(entry_path)) { + cleanup_dir(entry_path); + } + i = i + 1; + } + remove_dir(path); +} + +export fn run() { + suite("installer.get_installed"); + + test("returns empty array for non-existent hem_modules", fn() { + let installed = installer.get_installed("/tmp/hpm_test_nonexistent_dir_12345"); + assert_eq(installed.length, 0, "no packages"); + }); + + test("returns empty array for empty hem_modules", fn() { + let test_dir = "/tmp/hpm_test_empty_modules"; + cleanup_dir(test_dir); + make_dir(test_dir); + make_dir(test_dir + "/hem_modules"); + + let installed = installer.get_installed(test_dir); + assert_eq(installed.length, 0, "no packages in empty dir"); + + cleanup_dir(test_dir); + }); + + test("finds installed package with package.json", fn() { + let test_dir = "/tmp/hpm_test_with_pkg"; + cleanup_dir(test_dir); + + // Create directory structure + make_dir(test_dir); + make_dir(test_dir + "/hem_modules"); + make_dir(test_dir + "/hem_modules/testowner"); + make_dir(test_dir + "/hem_modules/testowner/testrepo"); + + // Write package.json + write_file(test_dir + "/hem_modules/testowner/testrepo/package.json", + "{\"name\": \"testowner/testrepo\", \"version\": \"1.2.3\"}"); + + let installed = installer.get_installed(test_dir); + assert_eq(installed.length, 1, "one package"); + assert_eq(installed[0].name, "testowner/testrepo", "package name"); + assert_eq(installed[0].version, "1.2.3", "package version"); + + cleanup_dir(test_dir); + }); + + test("finds multiple installed packages", fn() { + let test_dir = "/tmp/hpm_test_multi_pkg"; + cleanup_dir(test_dir); + + // Create directory structure + make_dir(test_dir); + make_dir(test_dir + "/hem_modules"); + make_dir(test_dir + "/hem_modules/owner1"); + make_dir(test_dir + "/hem_modules/owner1/repo1"); + make_dir(test_dir + "/hem_modules/owner2"); + make_dir(test_dir + "/hem_modules/owner2/repo2"); + + write_file(test_dir + "/hem_modules/owner1/repo1/package.json", + "{\"name\": \"owner1/repo1\", \"version\": \"1.0.0\"}"); + write_file(test_dir + "/hem_modules/owner2/repo2/package.json", + "{\"name\": \"owner2/repo2\", \"version\": \"2.0.0\"}"); + + let installed = installer.get_installed(test_dir); + assert_eq(installed.length, 2, "two packages"); + + cleanup_dir(test_dir); + }); + + test("handles package without package.json", fn() { + let test_dir = "/tmp/hpm_test_no_pkgjson"; + cleanup_dir(test_dir); + + make_dir(test_dir); + make_dir(test_dir + "/hem_modules"); + make_dir(test_dir + "/hem_modules/owner"); + make_dir(test_dir + "/hem_modules/owner/repo"); + // No package.json file + + let installed = installer.get_installed(test_dir); + assert_eq(installed.length, 1, "found package dir"); + assert_eq(installed[0].version, "unknown", "unknown version"); + + cleanup_dir(test_dir); + }); + + test("handles multiple repos under same owner", fn() { + let test_dir = "/tmp/hpm_test_same_owner"; + cleanup_dir(test_dir); + + make_dir(test_dir); + make_dir(test_dir + "/hem_modules"); + make_dir(test_dir + "/hem_modules/hemlang"); + make_dir(test_dir + "/hem_modules/hemlang/sprout"); + make_dir(test_dir + "/hem_modules/hemlang/utils"); + + write_file(test_dir + "/hem_modules/hemlang/sprout/package.json", + "{\"name\": \"hemlang/sprout\", \"version\": \"1.0.0\"}"); + write_file(test_dir + "/hem_modules/hemlang/utils/package.json", + "{\"name\": \"hemlang/utils\", \"version\": \"2.0.0\"}"); + + let installed = installer.get_installed(test_dir); + assert_eq(installed.length, 2, "two packages same owner"); + + cleanup_dir(test_dir); + }); + + suite("installer.verify_integrity"); + + test("returns empty issues for null lockfile", fn() { + let issues = installer.verify_integrity("/tmp/test", null); + assert_eq(issues.length, 0, "no issues for null"); + }); + + test("returns empty issues for lockfile without dependencies", fn() { + let lock = { dependencies: null }; + let issues = installer.verify_integrity("/tmp/test", lock); + assert_eq(issues.length, 0, "no issues for empty deps"); + }); + + test("detects missing package installation", fn() { + let test_dir = "/tmp/hpm_test_verify"; + cleanup_dir(test_dir); + make_dir(test_dir); + make_dir(test_dir + "/hem_modules"); + + let lock = { + dependencies: { + "hemlang/missing": { + version: "1.0.0", + resolved: "https://example.com/pkg.tar.gz", + integrity: "sha256-abc123" + } + } + }; + + let issues = installer.verify_integrity(test_dir, lock); + assert_eq(issues.length, 1, "one issue"); + assert_eq(issues[0].package, "hemlang/missing", "missing package"); + assert_eq(issues[0].issue, "not installed", "issue type"); + + cleanup_dir(test_dir); + }); + + test("no issues when package is installed", fn() { + let test_dir = "/tmp/hpm_test_verify_ok"; + cleanup_dir(test_dir); + + make_dir(test_dir); + make_dir(test_dir + "/hem_modules"); + make_dir(test_dir + "/hem_modules/hemlang"); + make_dir(test_dir + "/hem_modules/hemlang/installed"); + + let lock = { + dependencies: { + "hemlang/installed": { + version: "1.0.0", + resolved: "https://example.com/pkg.tar.gz", + integrity: "sha256-abc123" + } + } + }; + + let issues = installer.verify_integrity(test_dir, lock); + assert_eq(issues.length, 0, "no issues"); + + cleanup_dir(test_dir); + }); + + test("detects multiple missing packages", fn() { + let test_dir = "/tmp/hpm_test_verify_multi"; + cleanup_dir(test_dir); + make_dir(test_dir); + make_dir(test_dir + "/hem_modules"); + + let lock = { + dependencies: { + "owner/pkg1": { version: "1.0.0", resolved: "", integrity: "" }, + "owner/pkg2": { version: "2.0.0", resolved: "", integrity: "" }, + "owner/pkg3": { version: "3.0.0", resolved: "", integrity: "" } + } + }; + + let issues = installer.verify_integrity(test_dir, lock); + assert_eq(issues.length, 3, "three issues"); + + cleanup_dir(test_dir); + }); + + suite("installer.uninstall_package"); + + test("returns false for non-existent package", fn() { + let test_dir = "/tmp/hpm_test_uninstall_nonexist"; + cleanup_dir(test_dir); + make_dir(test_dir); + make_dir(test_dir + "/hem_modules"); + + let result = installer.uninstall_package("nonexistent/pkg", test_dir, false); + assert_false(result, "should return false"); + + cleanup_dir(test_dir); + }); + + test("removes installed package", fn() { + let test_dir = "/tmp/hpm_test_uninstall_ok"; + cleanup_dir(test_dir); + + make_dir(test_dir); + make_dir(test_dir + "/hem_modules"); + make_dir(test_dir + "/hem_modules/owner"); + make_dir(test_dir + "/hem_modules/owner/repo"); + write_file(test_dir + "/hem_modules/owner/repo/index.hml", "// code"); + + assert_true(exists(test_dir + "/hem_modules/owner/repo"), "package exists before"); + + let result = installer.uninstall_package("owner/repo", test_dir, false); + assert_true(result, "should return true"); + assert_false(exists(test_dir + "/hem_modules/owner/repo"), "package removed"); + + cleanup_dir(test_dir); + }); + + test("cleans up empty owner directory after uninstall", fn() { + let test_dir = "/tmp/hpm_test_uninstall_cleanup"; + cleanup_dir(test_dir); + + make_dir(test_dir); + make_dir(test_dir + "/hem_modules"); + make_dir(test_dir + "/hem_modules/singleowner"); + make_dir(test_dir + "/hem_modules/singleowner/onlyrepo"); + + installer.uninstall_package("singleowner/onlyrepo", test_dir, false); + assert_false(exists(test_dir + "/hem_modules/singleowner"), "owner dir cleaned up"); + + cleanup_dir(test_dir); + }); + + test("keeps owner directory if other repos exist", fn() { + let test_dir = "/tmp/hpm_test_uninstall_keep"; + cleanup_dir(test_dir); + + make_dir(test_dir); + make_dir(test_dir + "/hem_modules"); + make_dir(test_dir + "/hem_modules/owner"); + make_dir(test_dir + "/hem_modules/owner/repo1"); + make_dir(test_dir + "/hem_modules/owner/repo2"); + + installer.uninstall_package("owner/repo1", test_dir, false); + assert_true(exists(test_dir + "/hem_modules/owner"), "owner dir kept"); + assert_true(exists(test_dir + "/hem_modules/owner/repo2"), "sibling kept"); + + cleanup_dir(test_dir); + }); +} diff --git a/test/test_resolver.hml b/test/test_resolver.hml new file mode 100644 index 0000000..6818cb4 --- /dev/null +++ b/test/test_resolver.hml @@ -0,0 +1,287 @@ +// Tests for resolver module + +import { suite, test, assert, assert_eq, assert_true, assert_false, assert_null, assert_not_null, assert_throws } from "./framework.hml"; +import { HashMap, Set } from "@stdlib/collections"; +import * as resolver from "../src/resolver.hml"; + +export fn run() { + suite("resolver.detect_cycles"); + + test("returns null when no cycles", fn() { + let resolved = HashMap(); + resolved.set("hemlang/a", { + version: "1.0.0", + dependencies: { "hemlang/b": "^1.0.0" } + }); + resolved.set("hemlang/b", { + version: "1.0.0", + dependencies: null + }); + + let cycle = resolver.detect_cycles(resolved); + assert_null(cycle, "should not detect cycle"); + }); + + test("returns null for empty resolved map", fn() { + let resolved = HashMap(); + let cycle = resolver.detect_cycles(resolved); + assert_null(cycle, "empty map has no cycles"); + }); + + test("returns null for single package with no deps", fn() { + let resolved = HashMap(); + resolved.set("hemlang/solo", { + version: "1.0.0", + dependencies: null + }); + + let cycle = resolver.detect_cycles(resolved); + assert_null(cycle, "single package has no cycle"); + }); + + test("detects simple two-package cycle", fn() { + let resolved = HashMap(); + resolved.set("hemlang/a", { + version: "1.0.0", + dependencies: { "hemlang/b": "^1.0.0" } + }); + resolved.set("hemlang/b", { + version: "1.0.0", + dependencies: { "hemlang/a": "^1.0.0" } + }); + + let cycle = resolver.detect_cycles(resolved); + assert_not_null(cycle, "should detect cycle"); + assert_true(cycle.length >= 2, "cycle should have at least 2 elements"); + }); + + test("detects three-package cycle", fn() { + let resolved = HashMap(); + resolved.set("hemlang/a", { + version: "1.0.0", + dependencies: { "hemlang/b": "^1.0.0" } + }); + resolved.set("hemlang/b", { + version: "1.0.0", + dependencies: { "hemlang/c": "^1.0.0" } + }); + resolved.set("hemlang/c", { + version: "1.0.0", + dependencies: { "hemlang/a": "^1.0.0" } + }); + + let cycle = resolver.detect_cycles(resolved); + assert_not_null(cycle, "should detect cycle"); + assert_true(cycle.length >= 3, "cycle should have at least 3 elements"); + }); + + test("detects self-referencing cycle", fn() { + let resolved = HashMap(); + resolved.set("hemlang/self", { + version: "1.0.0", + dependencies: { "hemlang/self": "^1.0.0" } + }); + + let cycle = resolver.detect_cycles(resolved); + assert_not_null(cycle, "should detect self-reference"); + }); + + test("no cycle with diamond dependency", fn() { + // A -> B, A -> C, B -> D, C -> D (diamond, no cycle) + let resolved = HashMap(); + resolved.set("hemlang/a", { + version: "1.0.0", + dependencies: { "hemlang/b": "^1.0.0", "hemlang/c": "^1.0.0" } + }); + resolved.set("hemlang/b", { + version: "1.0.0", + dependencies: { "hemlang/d": "^1.0.0" } + }); + resolved.set("hemlang/c", { + version: "1.0.0", + dependencies: { "hemlang/d": "^1.0.0" } + }); + resolved.set("hemlang/d", { + version: "1.0.0", + dependencies: null + }); + + let cycle = resolver.detect_cycles(resolved); + assert_null(cycle, "diamond is not a cycle"); + }); + + suite("resolver.build_tree"); + + test("builds tree for root with no deps", fn() { + let manifest = { + name: "test/project", + version: "1.0.0", + dependencies: null, + devDependencies: null + }; + let resolved = HashMap(); + + let tree = resolver.build_tree(manifest, resolved, false); + assert_eq(tree.name, "test/project", "tree name"); + assert_eq(tree.version, "1.0.0", "tree version"); + assert_eq(tree.children.length, 0, "no children"); + }); + + test("builds tree with direct dependencies", fn() { + let manifest = { + name: "test/project", + version: "1.0.0", + dependencies: { "hemlang/a": "^1.0.0" }, + devDependencies: null + }; + let resolved = HashMap(); + resolved.set("hemlang/a", { + version: "1.2.0", + dependencies: null + }); + + let tree = resolver.build_tree(manifest, resolved, false); + assert_eq(tree.children.length, 1, "one child"); + assert_eq(tree.children[0].name, "hemlang/a", "child name"); + assert_eq(tree.children[0].version, "1.2.0", "child version"); + }); + + test("builds tree with transitive dependencies", fn() { + let manifest = { + name: "test/project", + version: "1.0.0", + dependencies: { "hemlang/a": "^1.0.0" }, + devDependencies: null + }; + let resolved = HashMap(); + resolved.set("hemlang/a", { + version: "1.0.0", + dependencies: { "hemlang/b": "^2.0.0" } + }); + resolved.set("hemlang/b", { + version: "2.1.0", + dependencies: null + }); + + let tree = resolver.build_tree(manifest, resolved, false); + assert_eq(tree.children.length, 1, "one direct child"); + assert_eq(tree.children[0].name, "hemlang/a", "first level"); + assert_eq(tree.children[0].children.length, 1, "has transitive"); + assert_eq(tree.children[0].children[0].name, "hemlang/b", "transitive name"); + }); + + test("includes dev dependencies when flag is true", fn() { + let manifest = { + name: "test/project", + version: "1.0.0", + dependencies: { "hemlang/prod": "^1.0.0" }, + devDependencies: { "hemlang/dev": "^1.0.0" } + }; + let resolved = HashMap(); + resolved.set("hemlang/prod", { version: "1.0.0", dependencies: null }); + resolved.set("hemlang/dev", { version: "1.0.0", dependencies: null }); + + let tree = resolver.build_tree(manifest, resolved, true); + assert_eq(tree.children.length, 2, "both deps included"); + }); + + test("excludes dev dependencies when flag is false", fn() { + let manifest = { + name: "test/project", + version: "1.0.0", + dependencies: { "hemlang/prod": "^1.0.0" }, + devDependencies: { "hemlang/dev": "^1.0.0" } + }; + let resolved = HashMap(); + resolved.set("hemlang/prod", { version: "1.0.0", dependencies: null }); + resolved.set("hemlang/dev", { version: "1.0.0", dependencies: null }); + + let tree = resolver.build_tree(manifest, resolved, false); + assert_eq(tree.children.length, 1, "only prod dep"); + assert_eq(tree.children[0].name, "hemlang/prod", "prod dep name"); + }); + + suite("resolver.find_why"); + + test("finds direct dependency", fn() { + let manifest = { + name: "test/project", + version: "1.0.0", + dependencies: { "hemlang/direct": "^1.0.0" }, + devDependencies: null + }; + let resolved = HashMap(); + resolved.set("hemlang/direct", { version: "1.0.0", dependencies: null }); + + let chains = resolver.find_why("hemlang/direct", manifest, resolved); + assert_true(chains.length >= 1, "should find chain"); + }); + + test("finds dev dependency", fn() { + let manifest = { + name: "test/project", + version: "1.0.0", + dependencies: null, + devDependencies: { "hemlang/devpkg": "^1.0.0" } + }; + let resolved = HashMap(); + resolved.set("hemlang/devpkg", { version: "1.0.0", dependencies: null }); + + let chains = resolver.find_why("hemlang/devpkg", manifest, resolved); + assert_true(chains.length >= 1, "should find dev dep chain"); + }); + + test("finds transitive dependency chain", fn() { + let manifest = { + name: "test/project", + version: "1.0.0", + dependencies: { "hemlang/a": "^1.0.0" }, + devDependencies: null + }; + let resolved = HashMap(); + resolved.set("hemlang/a", { + version: "1.0.0", + dependencies: { "hemlang/b": "^1.0.0" } + }); + resolved.set("hemlang/b", { version: "1.0.0", dependencies: null }); + + let chains = resolver.find_why("hemlang/b", manifest, resolved); + assert_true(chains.length >= 1, "should find transitive chain"); + }); + + test("returns empty for unknown package", fn() { + let manifest = { + name: "test/project", + version: "1.0.0", + dependencies: { "hemlang/a": "^1.0.0" }, + devDependencies: null + }; + let resolved = HashMap(); + resolved.set("hemlang/a", { version: "1.0.0", dependencies: null }); + + let chains = resolver.find_why("hemlang/unknown", manifest, resolved); + assert_eq(chains.length, 0, "no chains for unknown"); + }); + + test("handles deep dependency chain", fn() { + let manifest = { + name: "test/project", + version: "1.0.0", + dependencies: { "hemlang/level1": "^1.0.0" }, + devDependencies: null + }; + let resolved = HashMap(); + resolved.set("hemlang/level1", { + version: "1.0.0", + dependencies: { "hemlang/level2": "^1.0.0" } + }); + resolved.set("hemlang/level2", { + version: "1.0.0", + dependencies: { "hemlang/level3": "^1.0.0" } + }); + resolved.set("hemlang/level3", { version: "1.0.0", dependencies: null }); + + let chains = resolver.find_why("hemlang/level3", manifest, resolved); + assert_true(chains.length >= 1, "should find deep chain"); + }); +}