diff --git a/atlas.nimble b/atlas.nimble index 2b89968e..d9de29ad 100644 --- a/atlas.nimble +++ b/atlas.nimble @@ -1,5 +1,5 @@ # Package -version = "0.9.1" +version = "0.9.2" author = "Araq" description = "Atlas is a simple package cloner tool. It manages an isolated project." license = "MIT" diff --git a/config.nims b/config.nims index dd5ebd6a..d2132698 100644 --- a/config.nims +++ b/config.nims @@ -18,6 +18,7 @@ task unitTests, "Runs unit tests": exec "nim c -d:debug -r tests/testsemverUnit.nim" exec "nim c -d:debug -r tests/testautoinit.nim" exec "nim c -d:debug -r tests/testsearch.nim" + exec "nim c -d:debug -r tests/truntests.nim" task tester, "Runs integration tests": exec "nim c -d:debug -r tests/tester.nim" diff --git a/doc/atlas.md b/doc/atlas.md index 8e6fb08e..57088d06 100644 --- a/doc/atlas.md +++ b/doc/atlas.md @@ -104,6 +104,11 @@ For example: ``` +### Test [--parallel] [paths...] + +Use `atlas test` to run all tests in `tests/*.nim`. Pass Nim files to run specific tests: `atlas test tests/mytest.nim`. + +Tests can be run in parallel: `atlas test --parallel`. Note that the test output is *not* synchronized. ### Link diff --git a/readme.md b/readme.md index 05f119b9..a458bfd9 100644 --- a/readme.md +++ b/readme.md @@ -8,8 +8,14 @@ Upcoming Nim version 2.0 will ship with `atlas`. Building from source: ```sh git clone https://github.com/nim-lang/atlas.git cd atlas -nim c src/atlas.nim -# copy src/atlas[.exe] somewhere in your PATH +nim build +# copy bin/atlas[.exe] somewhere in your PATH +``` + +Or with Nimble: + +```sh +nimble install https://github.com/nim-lang/atlas@\#head ``` # Documentation @@ -22,8 +28,7 @@ Create a new project. A project contains everything we need and can safely be de this tutorial: ```sh -mkdir project -cd project +mkdir project && cd project atlas init ``` @@ -47,12 +52,13 @@ echo "import malebolgia" >myproject.nim nim c myproject.nim ``` -The project structure looks like this: +### Project Structure -``` +```sh $project / project.nimble - $project / nim.cfg + $project / nim.cfg # Atlas generated file $project / other main project files... + $project / deps / # Folder where deps are stored $project / deps / atlas.config $project / deps / dependency-A $project / deps / dependency-B diff --git a/src/atlas.nim b/src/atlas.nim index 555d1040..386e5fff 100644 --- a/src/atlas.nim +++ b/src/atlas.nim @@ -9,10 +9,10 @@ ## Simple tool to automate frequent workflows: Can "clone" ## a Nimble dependency and its dependencies recursively. -import std / [parseopt, files, dirs, strutils, os, osproc, tables, sets, json, uri, paths] +import std / [parseopt, files, dirs, strutils, os, osproc, tables, sets, json, uri, paths, algorithm] import basic / [versions, context, osutils, configutils, reporters, nimbleparser, gitops, pkgurls, nimblecontext, compiledpatterns, packageinfos] -import depgraphs, nimenv, lockfiles, confighandler, dependencies, pkgsearch +import depgraphs, nimenv, lockfiles, confighandler, dependencies, pkgsearch, runtests from std/terminal import isatty @@ -49,8 +49,6 @@ Command: update update a package and all of its dependencies search [keyB ...] search for package that contains the given keywords - extract extract the requirements and custom commands from - the given Nimble file updateDeps [filter] update every dependency that has a remote URL that matches `filter` if a filter is given tag [major|minor|patch] @@ -61,8 +59,7 @@ Command: rep [atlas.lock] replay the state of the projects according to the lock file changed list any packages that differ from the lock file outdated list the packages that are outdated - build|test|doc|tasks currently delegates to `nimble build|test|doc` - task currently delegates to `nimble ` + test [tests...] run all tests `tests/t*.nim` or specified tests; supports `--parallel` env setup a Nim virtual environment --keep keep the c_code subdirectory @@ -70,6 +67,8 @@ Options: --feature= enables the given feature, pass multiple for multiple features for project specific use: `feature..` (note always be passed when you want to use features) + --parallel enables parallel execution on some tasks using countProcessors() + --parallel: enables parallel execution using `N` processes --keepCommits do not perform any `git checkouts` --noexec do not perform any action that may run arbitrary code --autoenv detect the minimal Nim $version and setup a @@ -459,6 +458,15 @@ proc parseAtlasOptions(params: seq[string], action: var string, args: var seq[st of "maxver": context().defaultAlgo = MaxVer of "semver": context().defaultAlgo = SemVer else: writeHelp() + of "parallel": + # number of parallel test commands to run + if val != "": + try: + context().parallelCount = parseInt(val) + except CatchableError: + fatal "Invalid value for --parallel: '" & val & "'" + else: + context().parallelCount = countProcessors() of "verbosity": case val.normalize of "normal": setAtlasVerbosity(Info) @@ -489,6 +497,21 @@ proc parseAtlasOptions(params: seq[string], action: var string, args: var seq[st createDir(depsDir()) proc atlasRun*(params: seq[string]) = + # Support forwarding args after "--" to subcommands like `test`. + var mainParams: seq[string] = @[] + var postDashParams: seq[string] = @[] + var seenDashDash = false + for p in params: + if not seenDashDash and p == "--": + seenDashDash = true + continue + if seenDashDash: + postDashParams.add p + else: + mainParams.add p + + context().extraParams = postDashParams + var action = "" var args: seq[string] = @[] template singleArg() = @@ -505,7 +528,7 @@ proc atlasRun*(params: seq[string]) = if args.len != 0: fatal action & " command takes no arguments" - parseAtlasOptions(params, action, args) + parseAtlasOptions(mainParams, action, args) if action notin ["init", "tag", "search", "list"]: doAssert project().string != "" and project().dirExists(), "project was not set" @@ -616,6 +639,15 @@ proc atlasRun*(params: seq[string]) = setupNimEnv args[0], KeepNimEnv in context().flags of "outdated": listOutdated() + of "test": + let runCode = NoExec notin context().flags + var testsToRun: seq[string] = @[] + for a in args: + if not a.startsWith("-") and a.endsWith(".nim"): + testsToRun.add a + let code = runTests(project(), postDashParams, runCode, context().parallelCount, testsToRun) + if code != 0: + quit(code) else: fatal "Invalid action: " & action diff --git a/src/basic/context.nim b/src/basic/context.nim index ea506d11..634d8f03 100644 --- a/src/basic/context.nim +++ b/src/basic/context.nim @@ -66,6 +66,8 @@ type pluginsFile*: Path proxy*: Uri features*: HashSet[string] + extraParams*: seq[string] + parallelCount*: Natural var atlasContext = AtlasContext() diff --git a/src/runtests.nim b/src/runtests.nim new file mode 100644 index 00000000..0e1ec49c --- /dev/null +++ b/src/runtests.nim @@ -0,0 +1,116 @@ +import std/[osproc, os, strutils, sequtils, math, paths, algorithm] +import basic/reporters + +## Parallel test runner utilities using osproc.execProcesses. + +proc buildTestCommand*(nimPath, testFile: string; extraArgs: seq[string] = @[]; runCode = true): string = + ## Compose a Nim compile (and optional run) command for a test file. + var cmd = quoteShell(nimPath) & " c -d:debug" + if extraArgs.len > 0: + for a in extraArgs: + cmd.add " " & quoteShell(a) + if runCode: + cmd.add " -r" + cmd.add " " & quoteShell(testFile) + result = cmd + +proc runCommandsParallel*(commands: seq[string]; parallel: int = 0): int = + ## Run commands with limited parallelism; returns first non‑zero exit code or 0. + ## If `parallel <= 0`, uses `countProcessors()`; otherwise, runs in batches of `parallel`. + if commands.len == 0: + return 0 + if parallel <= 0: + return execProcesses(commands, n = countProcessors(), + beforeRunEvent = proc (idx: int) = discard, + afterRunEvent = proc (idx: int, p: Process) = discard + ) + var i = 0 + while i < commands.len: + let j = min(i + parallel, commands.len) + let code = execProcesses(commands[i ..< j], n = parallel, + beforeRunEvent = proc (idx: int) = discard, + afterRunEvent = proc (idx: int, p: Process) = discard + ) + if code != 0: + return code + i = j + return 0 + +proc runTestsParallel*(tests: seq[string]; nimPath = findExe("nim"); extraArgs: seq[string] = @[]; runCode = true; parallel: int = countProcessors()): int = + ## Build and execute Nim test commands in parallel. + if nimPath.len == 0: + return -1 + var cmds: seq[string] = @[] + for tf in tests: + cmds.add buildTestCommand(nimPath, tf, extraArgs, runCode) + result = runCommandsParallel(cmds, parallel) + +proc discoverTests*(projectDir: Path): seq[string] = + ## Find and return all tests matching tests/t*.nim inside projectDir. + let old = os.getCurrentDir() + defer: os.setCurrentDir(old) + os.setCurrentDir($projectDir) + for f in walkFiles("tests/t*.nim"): + result.add f + result.sort(system.cmp[string]) + +proc runTestsSerial*(projectDir: Path; extraArgs: seq[string] = @[]; runCode = true; onlyTests: seq[string] = @[]): int = + ## Sequentially compile and (optionally) run each discovered test. + if projectDir.len == 0 or not dirExists($projectDir): + fatal "No project directory detected", "atlas:test" + return 1 + let nimPath = findExe("nim") + if nimPath.len == 0: + fatal "Nim compiler not found in PATH", "atlas:test" + return 1 + let old = os.getCurrentDir() + defer: os.setCurrentDir(old) + os.setCurrentDir($projectDir) + var tests = + if onlyTests.len > 0: onlyTests + else: discoverTests(projectDir) + if tests.len == 0: + warn "atlas:test", "No tests found matching 'tests/t*.nim'" + return 0 + for tf in tests: + info "atlas:test", "running:", tf + let cmd = buildTestCommand(nimPath, tf, extraArgs, runCode) + let code = execShellCmd(cmd) + if code != 0: + fatal "Test failed: " & tf, "atlas:test", code + return code + notice "atlas:test", "All tests passed" + return 0 + +proc runTests*(projectDir: Path; extraArgs: seq[string] = @[]; runCode = true; parallel: int; onlyTests: seq[string] = @[]): int = + ## Run tests either serially or in parallel (parallel<=0 uses CPU count). + if projectDir.len == 0 or not dirExists($projectDir): + fatal "No project directory detected", "atlas:test" + return 1 + let nimPath = findExe("nim") + if nimPath.len == 0: + fatal "Nim compiler not found in PATH", "atlas:test" + return 1 + let tests = + if onlyTests.len > 0: onlyTests + else: discoverTests(projectDir) + if tests.len == 0: + warn "atlas:test", "No tests found matching 'tests/t*.nim'" + return 0 + if parallel > 1: + let code = runTestsParallel(tests, nimPath, extraArgs, runCode, parallel) + if code != 0: + fatal "A test failed", "atlas:test", code + else: + notice "atlas:test", "All tests passed" + return code + else: + return runTestsSerial(projectDir, extraArgs, runCode, tests) + +when isMainModule: + # Example usage: run all tests matching tests/t*.nim in parallel. + var tests: seq[string] = @[] + for f in walkFiles("tests/t*.nim"): + tests.add(f) + let code = runTestsParallel(tests) + quit(code) diff --git a/tests/testintegration.nim b/tests/testintegration.nim index 5350ea44..65e6a30e 100644 --- a/tests/testintegration.nim +++ b/tests/testintegration.nim @@ -6,7 +6,7 @@ import testerutils ensureGitHttpServer() -if execShellCmd("nim c -o:$# -d:release src/atlas.nim" % [atlasExe]) != 0: +if execShellCmd("nim c --nimcache:.nimcache -o:$# -d:release src/atlas.nim" % [atlasExe]) != 0: quit("FAILURE: compilation of atlas failed") proc integrationTest() = diff --git a/tests/truntests.nim b/tests/truntests.nim new file mode 100644 index 00000000..f2c95ad5 --- /dev/null +++ b/tests/truntests.nim @@ -0,0 +1,39 @@ +import std/[os, strutils, unittest] +import testerutils + +suite "atlas test runner": + # Ensure atlas binary exists + if execShellCmd("nim c --nimcache:.nimcache -o:$# -d:release src/atlas.nim" % [atlasExe]) != 0: + quit "Failed to build atlas binary" + + let ws = "tests/ws_runtests" + if not dirExists(ws): createDir(ws) + + withDir ws: + # Clean previous markers and prepare tests dir + if fileExists("ran_a.txt"): removeFile("ran_a.txt") + if fileExists("ran_b.txt"): removeFile("ran_b.txt") + if not dirExists("tests"): createDir("tests") + + test "runs all tests by default": + exec atlasExe & " --project:. test" + check fileExists("ran_a.txt") + check fileExists("ran_b.txt") + + # Reset markers + if fileExists("ran_a.txt"): removeFile("ran_a.txt") + if fileExists("ran_b.txt"): removeFile("ran_b.txt") + + test "runs a single specified test": + exec atlasExe & " --project:. test tests/ta.nim" + check fileExists("ran_a.txt") + check not fileExists("ran_b.txt") + + # Reset markers + if fileExists("ran_a.txt"): removeFile("ran_a.txt") + if fileExists("ran_b.txt"): removeFile("ran_b.txt") + + test "runs multiple specified tests": + exec atlasExe & " --project:. test tests/ta.nim tests/tb.nim" + check fileExists("ran_a.txt") + check fileExists("ran_b.txt") diff --git a/tests/ws_runtests/ran_a.txt b/tests/ws_runtests/ran_a.txt new file mode 100644 index 00000000..b5754e20 --- /dev/null +++ b/tests/ws_runtests/ran_a.txt @@ -0,0 +1 @@ +ok \ No newline at end of file diff --git a/tests/ws_runtests/ran_b.txt b/tests/ws_runtests/ran_b.txt new file mode 100644 index 00000000..b5754e20 --- /dev/null +++ b/tests/ws_runtests/ran_b.txt @@ -0,0 +1 @@ +ok \ No newline at end of file diff --git a/tests/ws_runtests/tests/ta.nim b/tests/ws_runtests/tests/ta.nim new file mode 100644 index 00000000..a34d43ca --- /dev/null +++ b/tests/ws_runtests/tests/ta.nim @@ -0,0 +1,5 @@ +import std/[unittest, os] +suite "A": + test "a": + writeFile("ran_a.txt", "ok") + check true diff --git a/tests/ws_runtests/tests/tb.nim b/tests/ws_runtests/tests/tb.nim new file mode 100644 index 00000000..7af9ac84 --- /dev/null +++ b/tests/ws_runtests/tests/tb.nim @@ -0,0 +1,5 @@ +import std/[unittest, os] +suite "B": + test "b": + writeFile("ran_b.txt", "ok") + check true