From 1e46faef8b97161263421fc01373789e954180f8 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Mon, 5 Jan 2026 21:35:41 -0500 Subject: [PATCH] Add unit tests for 18 languages --- .github/workflows/inception-matrix.yml | 142 +++++++++++++++-- tests/unit/run_all.sh | 20 +++ tests/unit/test_clojure.clj | 146 +++++++++++++++++ tests/unit/test_dart.dart | 178 +++++++++++++++++++++ tests/unit/test_elixir.exs | 186 ++++++++++++++++++++++ tests/unit/test_erlang.erl | 168 ++++++++++++++++++++ tests/unit/test_julia.jl | 178 +++++++++++++++++++++ tests/unit/test_perl.pl | 212 +++++++++++++++++++++++++ tests/unit/test_php.php | 210 ++++++++++++++++++++++++ tests/unit/test_powershell.ps1 | 192 ++++++++++++++++++++++ tests/unit/test_r.R | 179 +++++++++++++++++++++ tests/unit/test_tcl.tcl | 183 +++++++++++++++++++++ tests/unit/test_typescript.ts | 191 ++++++++++++++++++++++ 13 files changed, 2169 insertions(+), 16 deletions(-) create mode 100755 tests/unit/test_clojure.clj create mode 100755 tests/unit/test_dart.dart create mode 100755 tests/unit/test_elixir.exs create mode 100755 tests/unit/test_erlang.erl create mode 100755 tests/unit/test_julia.jl create mode 100755 tests/unit/test_perl.pl create mode 100755 tests/unit/test_php.php create mode 100755 tests/unit/test_powershell.ps1 create mode 100755 tests/unit/test_r.R create mode 100755 tests/unit/test_tcl.tcl create mode 100755 tests/unit/test_typescript.ts diff --git a/.github/workflows/inception-matrix.yml b/.github/workflows/inception-matrix.yml index 2e68acb..f24b9ab 100644 --- a/.github/workflows/inception-matrix.yml +++ b/.github/workflows/inception-matrix.yml @@ -21,8 +21,8 @@ jobs: # ============================================================================ # UNIT TESTS - Test internal logic without API calls # ============================================================================ - unit-tests: - name: "Unit Tests: ${{ matrix.lang }}" + unit-tests-scripting: + name: "Unit: ${{ matrix.lang }}" runs-on: ubuntu-latest strategy: fail-fast: false @@ -34,35 +34,145 @@ jobs: run: node tests/unit/test_javascript.js - lang: Ruby run: ruby tests/unit/test_ruby.rb - - lang: Go - run: go run tests/unit/test_go.go - lang: Bash run: bash tests/unit/test_bash.sh - steps: - uses: actions/checkout@v4 - - - name: Setup Go - if: matrix.lang == 'Go' - uses: actions/setup-go@v5 - with: - go-version: '1.22' - - name: Run ${{ matrix.lang }} unit tests run: ${{ matrix.run }} + unit-tests-go: + name: "Unit: Go" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.22' + - name: Run Go unit tests + run: go run tests/unit/test_go.go + unit-tests-lua: - name: "Unit Tests: Lua" + name: "Unit: Lua" runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Lua - run: | - sudo apt-get update - sudo apt-get install -y lua5.4 + run: sudo apt-get update && sudo apt-get install -y lua5.4 - name: Run Lua unit tests run: lua tests/unit/test_lua.lua + unit-tests-php: + name: "Unit: PHP" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + - name: Run PHP unit tests + run: php tests/unit/test_php.php + + unit-tests-perl: + name: "Unit: Perl" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run Perl unit tests + run: perl tests/unit/test_perl.pl + + unit-tests-elixir: + name: "Unit: Elixir" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: '26' + elixir-version: '1.15' + - name: Run Elixir unit tests + run: elixir tests/unit/test_elixir.exs + + unit-tests-erlang: + name: "Unit: Erlang" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: '26' + - name: Run Erlang unit tests + run: escript tests/unit/test_erlang.erl + + unit-tests-julia: + name: "Unit: Julia" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v1 + with: + version: '1' + - name: Run Julia unit tests + run: julia tests/unit/test_julia.jl + + unit-tests-r: + name: "Unit: R" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: r-lib/actions/setup-r@v2 + - name: Install R packages + run: Rscript -e 'install.packages(c("openssl", "base64enc"), repos="https://cloud.r-project.org")' + - name: Run R unit tests + run: Rscript tests/unit/test_r.R + + unit-tests-clojure: + name: "Unit: Clojure" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Clojure + run: | + curl -L -O https://github.com/clojure/brew-install/releases/latest/download/linux-install.sh + chmod +x linux-install.sh + sudo ./linux-install.sh + - name: Run Clojure unit tests + run: clj -M tests/unit/test_clojure.clj + + unit-tests-dart: + name: "Unit: Dart" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dart-lang/setup-dart@v1 + - name: Install crypto package + run: | + cd tests/unit + echo 'name: test_dart' > pubspec.yaml + echo 'dependencies:' >> pubspec.yaml + echo ' crypto: ^3.0.0' >> pubspec.yaml + dart pub get + - name: Run Dart unit tests + run: cd tests/unit && dart run test_dart.dart + + unit-tests-tcl: + name: "Unit: TCL" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install TCL + run: sudo apt-get update && sudo apt-get install -y tcl + - name: Run TCL unit tests + run: tclsh tests/unit/test_tcl.tcl + + unit-tests-powershell: + name: "Unit: PowerShell" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run PowerShell unit tests + run: pwsh tests/unit/test_powershell.ps1 + # ============================================================================ # INTEGRATION TESTS - Test component interactions without real API calls # ============================================================================ diff --git a/tests/unit/run_all.sh b/tests/unit/run_all.sh index 7f4112c..766d23c 100755 --- a/tests/unit/run_all.sh +++ b/tests/unit/run_all.sh @@ -47,7 +47,10 @@ run_test() { echo -e "${CYAN}━━━ Scripting Languages ━━━${NC}" run_test "Python" "python3 test_python.py" "python3" run_test "JavaScript" "node test_javascript.js" "node" +run_test "TypeScript" "npx ts-node test_typescript.ts" "npx" run_test "Ruby" "ruby test_ruby.rb" "ruby" +run_test "PHP" "php test_php.php" "php" +run_test "Perl" "perl test_perl.pl" "perl" run_test "Lua" "lua test_lua.lua" "lua" run_test "Bash" "bash test_bash.sh" "bash" echo "" @@ -56,6 +59,23 @@ echo -e "${CYAN}━━━ Systems Languages ━━━${NC}" run_test "Go" "go run test_go.go" "go" echo "" +echo -e "${CYAN}━━━ Functional Languages ━━━${NC}" +run_test "Elixir" "elixir test_elixir.exs" "elixir" +run_test "Erlang" "escript test_erlang.erl" "escript" +run_test "Clojure" "clj -M test_clojure.clj" "clj" +echo "" + +echo -e "${CYAN}━━━ Scientific Languages ━━━${NC}" +run_test "Julia" "julia test_julia.jl" "julia" +run_test "R" "Rscript test_r.R" "Rscript" +echo "" + +echo -e "${CYAN}━━━ Other Languages ━━━${NC}" +run_test "Dart" "dart run test_dart.dart" "dart" +run_test "TCL" "tclsh test_tcl.tcl" "tclsh" +run_test "PowerShell" "pwsh test_powershell.ps1" "pwsh" +echo "" + # Summary echo -e "${CYAN}══════════════════════════════════════════════════════════════${NC}" echo "" diff --git a/tests/unit/test_clojure.clj b/tests/unit/test_clojure.clj new file mode 100755 index 0000000..2e32b7d --- /dev/null +++ b/tests/unit/test_clojure.clj @@ -0,0 +1,146 @@ +#!/usr/bin/env clj -M +;; Unit tests for un.clj - tests internal functions without API calls + +(ns test-un-clj + (:import [javax.crypto Mac] + [javax.crypto.spec SecretKeySpec] + [java.util Base64])) + +(def passed (atom 0)) +(def failed (atom 0)) + +(defn test-case [name f] + (try + (f) + (println (str " ✓ " name)) + (swap! passed inc) + (catch Exception e + (println (str " ✗ " name)) + (println (str " " (.getMessage e))) + (swap! failed inc)))) + +(defn assert-equal [actual expected] + (when (not= actual expected) + (throw (Exception. (str "Expected '" expected "' but got '" actual "'"))))) + +(defn assert-not-equal [a b] + (when (= a b) + (throw (Exception. (str "Expected values to be different but both were '" a "'"))))) + +(defn assert-contains [s substr] + (when (not (.contains s substr)) + (throw (Exception. (str "Expected '" s "' to contain '" substr "'"))))) + +(defn assert-true [val] + (when (not val) + (throw (Exception. "Expected true but got false")))) + +(defn hmac-sha256 [secret message] + (let [mac (Mac/getInstance "HmacSHA256") + key (SecretKeySpec. (.getBytes secret "UTF-8") "HmacSHA256")] + (.init mac key) + (apply str (map #(format "%02x" %) (.doFinal mac (.getBytes message "UTF-8")))))) + +;; Extension mapping +(def ext-map + {".py" "python" ".js" "javascript" ".ts" "typescript" + ".rb" "ruby" ".php" "php" ".pl" "perl" ".lua" "lua" + ".sh" "bash" ".go" "go" ".rs" "rust" ".c" "c" + ".cpp" "cpp" ".java" "java" ".kt" "kotlin" + ".hs" "haskell" ".clj" "clojure" ".erl" "erlang" + ".ex" "elixir" ".jl" "julia" ".r" "r"}) + +(println "\n=== Extension Mapping Tests ===") + +(test-case "Python extension maps correctly" + #(assert-equal (ext-map ".py") "python")) + +(test-case "Clojure extension maps correctly" + #(assert-equal (ext-map ".clj") "clojure")) + +(test-case "JavaScript extension maps correctly" + #(assert-equal (ext-map ".js") "javascript")) + +(test-case "Go extension maps correctly" + #(assert-equal (ext-map ".go") "go")) + +(test-case "Haskell extension maps correctly" + #(assert-equal (ext-map ".hs") "haskell")) + +(println "\n=== HMAC Signature Tests ===") + +(test-case "HMAC-SHA256 generates 64 character hex string" + #(let [sig (hmac-sha256 "test-secret" "test-message")] + (assert-equal (count sig) 64))) + +(test-case "Same input produces same signature" + #(let [sig1 (hmac-sha256 "key" "msg") + sig2 (hmac-sha256 "key" "msg")] + (assert-equal sig1 sig2))) + +(test-case "Different secrets produce different signatures" + #(let [sig1 (hmac-sha256 "key1" "msg") + sig2 (hmac-sha256 "key2" "msg")] + (assert-not-equal sig1 sig2))) + +(test-case "Signature format verification" + #(let [timestamp "1704067200" + method "POST" + endpoint "/execute" + body "{\"language\":\"python\"}" + message (str timestamp ":" method ":" endpoint ":" body)] + (assert-true (.startsWith message timestamp)) + (assert-contains message ":POST:") + (assert-contains message ":/execute:"))) + +(println "\n=== Language Detection Tests ===") + +(test-case "Detect language from .clj extension" + #(let [filename "script.clj" + ext (str "." (last (clojure.string/split filename #"\.")))] + (assert-equal (ext-map ext) "clojure"))) + +(test-case "Python shebang detection" + #(let [content "#!/usr/bin/env python3\nprint('hello')" + first-line (first (clojure.string/split content #"\n"))] + (assert-true (.startsWith first-line "#!")) + (assert-contains first-line "python"))) + +(println "\n=== Argument Parsing Tests ===") + +(test-case "Parse -e KEY=VALUE format" + #(let [arg "DEBUG=1" + [key value] (clojure.string/split arg #"=" 2)] + (assert-equal key "DEBUG") + (assert-equal value "1"))) + +(test-case "Parse -e KEY=VALUE with equals in value" + #(let [arg "URL=https://example.com?foo=bar" + [key value] (clojure.string/split arg #"=" 2)] + (assert-equal key "URL") + (assert-equal value "https://example.com?foo=bar"))) + +(println "\n=== File Operations Tests ===") + +(test-case "Base64 encoding/decoding" + #(let [content "print('hello world')" + encoder (Base64/getEncoder) + decoder (Base64/getDecoder) + encoded (.encodeToString encoder (.getBytes content)) + decoded (String. (.decode decoder encoded))] + (assert-equal decoded content))) + +(println "\n=== API Constants Tests ===") + +(test-case "API base URL format" + #(let [api-base "https://api.unsandbox.com"] + (assert-true (.startsWith api-base "https://")) + (assert-contains api-base "unsandbox.com"))) + +;; Summary +(println "\n=== Summary ===") +(println (str "Passed: " @passed)) +(println (str "Failed: " @failed)) +(println (str "Total: " (+ @passed @failed))) + +(System/exit (if (> @failed 0) 1 0)) diff --git a/tests/unit/test_dart.dart b/tests/unit/test_dart.dart new file mode 100755 index 0000000..d5320b8 --- /dev/null +++ b/tests/unit/test_dart.dart @@ -0,0 +1,178 @@ +#!/usr/bin/env dart +/// Unit tests for un.dart - tests internal functions without API calls + +import 'dart:convert'; +import 'dart:io'; +import 'package:crypto/crypto.dart'; + +int passed = 0; +int failed = 0; + +void test(String name, void Function() fn) { + try { + fn(); + print(' ✓ $name'); + passed++; + } catch (e) { + print(' ✗ $name'); + print(' $e'); + failed++; + } +} + +void assertEqual(dynamic actual, dynamic expected) { + if (actual != expected) { + throw Exception("Expected '$expected' but got '$actual'"); + } +} + +void assertNotEqual(dynamic a, dynamic b) { + if (a == b) { + throw Exception("Expected values to be different but both were '$a'"); + } +} + +void assertContains(String str, String substr) { + if (!str.contains(substr)) { + throw Exception("Expected '$str' to contain '$substr'"); + } +} + +void assertTrue(bool val) { + if (!val) { + throw Exception("Expected true but got false"); + } +} + +String hmacSha256(String secret, String message) { + final key = utf8.encode(secret); + final bytes = utf8.encode(message); + final hmac = Hmac(sha256, key); + final digest = hmac.convert(bytes); + return digest.toString(); +} + +void main() { + final extMap = { + '.py': 'python', '.js': 'javascript', '.ts': 'typescript', + '.rb': 'ruby', '.php': 'php', '.pl': 'perl', '.lua': 'lua', + '.sh': 'bash', '.go': 'go', '.rs': 'rust', '.c': 'c', + '.cpp': 'cpp', '.java': 'java', '.kt': 'kotlin', + '.hs': 'haskell', '.clj': 'clojure', '.erl': 'erlang', + '.ex': 'elixir', '.jl': 'julia', '.dart': 'dart', + }; + + print('\n=== Extension Mapping Tests ==='); + + test('Python extension maps correctly', () { + assertEqual(extMap['.py'], 'python'); + }); + + test('Dart extension maps correctly', () { + assertEqual(extMap['.dart'], 'dart'); + }); + + test('JavaScript extension maps correctly', () { + assertEqual(extMap['.js'], 'javascript'); + }); + + test('Go extension maps correctly', () { + assertEqual(extMap['.go'], 'go'); + }); + + test('Kotlin extension maps correctly', () { + assertEqual(extMap['.kt'], 'kotlin'); + }); + + print('\n=== HMAC Signature Tests ==='); + + test('HMAC-SHA256 generates 64 character hex string', () { + final sig = hmacSha256('test-secret', 'test-message'); + assertEqual(sig.length, 64); + }); + + test('Same input produces same signature', () { + final sig1 = hmacSha256('key', 'msg'); + final sig2 = hmacSha256('key', 'msg'); + assertEqual(sig1, sig2); + }); + + test('Different secrets produce different signatures', () { + final sig1 = hmacSha256('key1', 'msg'); + final sig2 = hmacSha256('key2', 'msg'); + assertNotEqual(sig1, sig2); + }); + + test('Signature format verification', () { + final timestamp = '1704067200'; + final method = 'POST'; + final endpoint = '/execute'; + final body = '{"language":"python"}'; + + final message = '$timestamp:$method:$endpoint:$body'; + + assertTrue(message.startsWith(timestamp)); + assertContains(message, ':POST:'); + assertContains(message, ':/execute:'); + }); + + print('\n=== Language Detection Tests ==='); + + test('Detect language from .dart extension', () { + final filename = 'script.dart'; + final ext = '.${filename.split('.').last}'; + assertEqual(extMap[ext], 'dart'); + }); + + test('Python shebang detection', () { + final content = "#!/usr/bin/env python3\nprint('hello')"; + final firstLine = content.split('\n')[0]; + assertTrue(firstLine.startsWith('#!')); + assertContains(firstLine, 'python'); + }); + + print('\n=== Argument Parsing Tests ==='); + + test('Parse -e KEY=VALUE format', () { + final arg = 'DEBUG=1'; + final parts = arg.split('='); + final key = parts[0]; + final value = parts.sublist(1).join('='); + assertEqual(key, 'DEBUG'); + assertEqual(value, '1'); + }); + + test('Parse -e KEY=VALUE with equals in value', () { + final arg = 'URL=https://example.com?foo=bar'; + final parts = arg.split('='); + final key = parts[0]; + final value = parts.sublist(1).join('='); + assertEqual(key, 'URL'); + assertEqual(value, 'https://example.com?foo=bar'); + }); + + print('\n=== File Operations Tests ==='); + + test('Base64 encoding/decoding', () { + final content = "print('hello world')"; + final encoded = base64Encode(utf8.encode(content)); + final decoded = utf8.decode(base64Decode(encoded)); + assertEqual(decoded, content); + }); + + print('\n=== API Constants Tests ==='); + + test('API base URL format', () { + final apiBase = 'https://api.unsandbox.com'; + assertTrue(apiBase.startsWith('https://')); + assertContains(apiBase, 'unsandbox.com'); + }); + + // Summary + print('\n=== Summary ==='); + print('Passed: $passed'); + print('Failed: $failed'); + print('Total: ${passed + failed}'); + + exit(failed > 0 ? 1 : 0); +} diff --git a/tests/unit/test_elixir.exs b/tests/unit/test_elixir.exs new file mode 100755 index 0000000..3767148 --- /dev/null +++ b/tests/unit/test_elixir.exs @@ -0,0 +1,186 @@ +#!/usr/bin/env elixir +# Unit tests for un.ex - tests internal functions without API calls + +defmodule UnTest do + @passed Agent.start_link(fn -> 0 end, name: :passed) + @failed Agent.start_link(fn -> 0 end, name: :failed) + + def run do + Agent.start_link(fn -> 0 end, name: :passed) + Agent.start_link(fn -> 0 end, name: :failed) + + IO.puts("\n=== Extension Mapping Tests ===") + + ext_map = %{ + ".py" => "python", ".js" => "javascript", ".ts" => "typescript", + ".rb" => "ruby", ".php" => "php", ".pl" => "perl", ".lua" => "lua", + ".sh" => "bash", ".go" => "go", ".rs" => "rust", ".c" => "c", + ".cpp" => "cpp", ".java" => "java", ".kt" => "kotlin", + ".hs" => "haskell", ".clj" => "clojure", ".erl" => "erlang", + ".ex" => "elixir", ".exs" => "elixir", ".jl" => "julia" + } + + test("Python extension maps correctly", fn -> + assert_equal(ext_map[".py"], "python") + end) + + test("Elixir extensions map correctly", fn -> + assert_equal(ext_map[".ex"], "elixir") + assert_equal(ext_map[".exs"], "elixir") + end) + + test("JavaScript extension maps correctly", fn -> + assert_equal(ext_map[".js"], "javascript") + end) + + test("Go extension maps correctly", fn -> + assert_equal(ext_map[".go"], "go") + end) + + test("Erlang extension maps correctly", fn -> + assert_equal(ext_map[".erl"], "erlang") + end) + + IO.puts("\n=== HMAC Signature Tests ===") + + test("HMAC-SHA256 generates 64 character hex string", fn -> + sig = :crypto.mac(:hmac, :sha256, "test-secret", "test-message") + |> Base.encode16(case: :lower) + assert_equal(String.length(sig), 64) + end) + + test("Same input produces same signature", fn -> + sig1 = :crypto.mac(:hmac, :sha256, "key", "msg") |> Base.encode16(case: :lower) + sig2 = :crypto.mac(:hmac, :sha256, "key", "msg") |> Base.encode16(case: :lower) + assert_equal(sig1, sig2) + end) + + test("Different secrets produce different signatures", fn -> + sig1 = :crypto.mac(:hmac, :sha256, "key1", "msg") |> Base.encode16(case: :lower) + sig2 = :crypto.mac(:hmac, :sha256, "key2", "msg") |> Base.encode16(case: :lower) + assert_not_equal(sig1, sig2) + end) + + test("Signature format verification", fn -> + timestamp = "1704067200" + method = "POST" + endpoint = "/execute" + body = ~s({"language":"python"}) + + message = "#{timestamp}:#{method}:#{endpoint}:#{body}" + + assert_true(String.starts_with?(message, timestamp)) + assert_contains(message, ":POST:") + assert_contains(message, ":/execute:") + end) + + IO.puts("\n=== Language Detection Tests ===") + + test("Detect language from .exs extension", fn -> + ext = Path.extname("script.exs") + assert_equal(ext_map[ext], "elixir") + end) + + test("Python shebang detection", fn -> + content = "#!/usr/bin/env python3\nprint('hello')" + [first_line | _] = String.split(content, "\n") + assert_true(String.starts_with?(first_line, "#!")) + assert_contains(first_line, "python") + end) + + IO.puts("\n=== Argument Parsing Tests ===") + + test("Parse -e KEY=VALUE format", fn -> + arg = "DEBUG=1" + [key | rest] = String.split(arg, "=", parts: 2) + value = Enum.join(rest, "=") + assert_equal(key, "DEBUG") + assert_equal(value, "1") + end) + + test("Parse -e KEY=VALUE with equals in value", fn -> + arg = "URL=https://example.com?foo=bar" + [key | rest] = String.split(arg, "=", parts: 2) + value = Enum.join(rest, "=") + assert_equal(key, "URL") + assert_equal(value, "https://example.com?foo=bar") + end) + + IO.puts("\n=== File Operations Tests ===") + + test("Base64 encoding/decoding", fn -> + content = "print('hello world')" + encoded = Base.encode64(content) + {:ok, decoded} = Base.decode64(encoded) + assert_equal(decoded, content) + end) + + test("Extract file basename", fn -> + path = "/home/user/project/script.exs" + assert_equal(Path.basename(path), "script.exs") + end) + + test("Extract file extension", fn -> + path = "/home/user/project/script.exs" + assert_equal(Path.extname(path), ".exs") + end) + + IO.puts("\n=== API Constants Tests ===") + + test("API base URL format", fn -> + api_base = "https://api.unsandbox.com" + assert_true(String.starts_with?(api_base, "https://")) + assert_contains(api_base, "unsandbox.com") + end) + + # Summary + passed = Agent.get(:passed, & &1) + failed = Agent.get(:failed, & &1) + + IO.puts("\n=== Summary ===") + IO.puts("Passed: #{passed}") + IO.puts("Failed: #{failed}") + IO.puts("Total: #{passed + failed}") + + if failed > 0, do: System.halt(1), else: System.halt(0) + end + + defp test(name, fun) do + try do + fun.() + IO.puts(" ✓ #{name}") + Agent.update(:passed, &(&1 + 1)) + rescue + e -> + IO.puts(" ✗ #{name}") + IO.puts(" #{Exception.message(e)}") + Agent.update(:failed, &(&1 + 1)) + end + end + + defp assert_equal(actual, expected) do + if actual != expected do + raise "Expected '#{expected}' but got '#{actual}'" + end + end + + defp assert_not_equal(a, b) do + if a == b do + raise "Expected values to be different but both were '#{a}'" + end + end + + defp assert_contains(str, substr) do + unless String.contains?(str, substr) do + raise "Expected '#{str}' to contain '#{substr}'" + end + end + + defp assert_true(val) do + unless val do + raise "Expected true but got false" + end + end +end + +UnTest.run() diff --git a/tests/unit/test_erlang.erl b/tests/unit/test_erlang.erl new file mode 100755 index 0000000..c8ea298 --- /dev/null +++ b/tests/unit/test_erlang.erl @@ -0,0 +1,168 @@ +#!/usr/bin/env escript +%% Unit tests for un.erl - tests internal functions without API calls + +-mode(compile). + +main(_) -> + put(passed, 0), + put(failed, 0), + + io:format("~n=== Extension Mapping Tests ===~n"), + + ExtMap = #{ + ".py" => "python", ".js" => "javascript", ".ts" => "typescript", + ".rb" => "ruby", ".php" => "php", ".pl" => "perl", ".lua" => "lua", + ".sh" => "bash", ".go" => "go", ".rs" => "rust", ".c" => "c", + ".cpp" => "cpp", ".java" => "java", ".kt" => "kotlin", + ".hs" => "haskell", ".clj" => "clojure", ".erl" => "erlang", + ".ex" => "elixir", ".jl" => "julia" + }, + + test("Python extension maps correctly", fun() -> + assert_equal(maps:get(".py", ExtMap), "python") + end), + + test("Erlang extension maps correctly", fun() -> + assert_equal(maps:get(".erl", ExtMap), "erlang") + end), + + test("JavaScript extension maps correctly", fun() -> + assert_equal(maps:get(".js", ExtMap), "javascript") + end), + + test("Go extension maps correctly", fun() -> + assert_equal(maps:get(".go", ExtMap), "go") + end), + + io:format("~n=== HMAC Signature Tests ===~n"), + + test("HMAC-SHA256 generates 64 character hex string", fun() -> + Sig = crypto:mac(hmac, sha256, <<"test-secret">>, <<"test-message">>), + HexSig = binary_to_hex(Sig), + assert_equal(length(HexSig), 64) + end), + + test("Same input produces same signature", fun() -> + Sig1 = crypto:mac(hmac, sha256, <<"key">>, <<"msg">>), + Sig2 = crypto:mac(hmac, sha256, <<"key">>, <<"msg">>), + assert_equal(Sig1, Sig2) + end), + + test("Different secrets produce different signatures", fun() -> + Sig1 = crypto:mac(hmac, sha256, <<"key1">>, <<"msg">>), + Sig2 = crypto:mac(hmac, sha256, <<"key2">>, <<"msg">>), + assert_not_equal(Sig1, Sig2) + end), + + test("Signature format verification", fun() -> + Timestamp = "1704067200", + Method = "POST", + Endpoint = "/execute", + Body = "{\"language\":\"python\"}", + Message = Timestamp ++ ":" ++ Method ++ ":" ++ Endpoint ++ ":" ++ Body, + assert_true(string:prefix(Message, Timestamp) =/= nomatch), + assert_contains(Message, ":POST:"), + assert_contains(Message, ":/execute:") + end), + + io:format("~n=== Language Detection Tests ===~n"), + + test("Detect language from .erl extension", fun() -> + Ext = filename:extension("script.erl"), + assert_equal(maps:get(Ext, ExtMap), "erlang") + end), + + test("Python shebang detection", fun() -> + Content = "#!/usr/bin/env python3\nprint('hello')", + [FirstLine | _] = string:split(Content, "\n"), + assert_true(string:prefix(FirstLine, "#!") =/= nomatch), + assert_contains(FirstLine, "python") + end), + + io:format("~n=== Argument Parsing Tests ===~n"), + + test("Parse -e KEY=VALUE format", fun() -> + Arg = "DEBUG=1", + [Key, Value] = string:split(Arg, "="), + assert_equal(Key, "DEBUG"), + assert_equal(Value, "1") + end), + + io:format("~n=== File Operations Tests ===~n"), + + test("Base64 encoding/decoding", fun() -> + Content = "print('hello world')", + Encoded = base64:encode(Content), + Decoded = base64:decode(Encoded), + assert_equal(binary_to_list(Decoded), Content) + end), + + test("Extract file basename", fun() -> + Path = "/home/user/project/script.erl", + assert_equal(filename:basename(Path), "script.erl") + end), + + test("Extract file extension", fun() -> + Path = "/home/user/project/script.erl", + assert_equal(filename:extension(Path), ".erl") + end), + + io:format("~n=== API Constants Tests ===~n"), + + test("API base URL format", fun() -> + ApiBase = "https://api.unsandbox.com", + assert_true(string:prefix(ApiBase, "https://") =/= nomatch), + assert_contains(ApiBase, "unsandbox.com") + end), + + % Summary + Passed = get(passed), + Failed = get(failed), + io:format("~n=== Summary ===~n"), + io:format("Passed: ~p~n", [Passed]), + io:format("Failed: ~p~n", [Failed]), + io:format("Total: ~p~n", [Passed + Failed]), + + case Failed > 0 of + true -> halt(1); + false -> halt(0) + end. + +test(Name, Fun) -> + try + Fun(), + io:format(" ✓ ~s~n", [Name]), + put(passed, get(passed) + 1) + catch + _:Reason -> + io:format(" ✗ ~s~n", [Name]), + io:format(" ~p~n", [Reason]), + put(failed, get(failed) + 1) + end. + +assert_equal(Actual, Expected) -> + case Actual =:= Expected of + true -> ok; + false -> throw({expected, Expected, got, Actual}) + end. + +assert_not_equal(A, B) -> + case A =/= B of + true -> ok; + false -> throw({expected_different, A}) + end. + +assert_contains(Str, Substr) -> + case string:find(Str, Substr) of + nomatch -> throw({expected_to_contain, Str, Substr}); + _ -> ok + end. + +assert_true(Val) -> + case Val of + true -> ok; + _ -> throw({expected_true, got, Val}) + end. + +binary_to_hex(Bin) -> + lists:flatten([io_lib:format("~2.16.0b", [X]) || <> <= Bin]). diff --git a/tests/unit/test_julia.jl b/tests/unit/test_julia.jl new file mode 100755 index 0000000..0989cdb --- /dev/null +++ b/tests/unit/test_julia.jl @@ -0,0 +1,178 @@ +#!/usr/bin/env julia +# Unit tests for un.jl - tests internal functions without API calls + +using SHA +using Base64 + +passed = Ref(0) +failed = Ref(0) + +function test(name, fn) + try + fn() + println(" ✓ $name") + passed[] += 1 + catch e + println(" ✗ $name") + println(" $(sprint(showerror, e))") + failed[] += 1 + end +end + +function assert_equal(actual, expected) + if actual != expected + error("Expected '$expected' but got '$actual'") + end +end + +function assert_not_equal(a, b) + if a == b + error("Expected values to be different but both were '$a'") + end +end + +function assert_contains(str, substr) + if !occursin(substr, str) + error("Expected '$str' to contain '$substr'") + end +end + +function assert_true(val) + if !val + error("Expected true but got false") + end +end + +# Extension mapping +EXT_MAP = Dict( + ".py" => "python", ".js" => "javascript", ".ts" => "typescript", + ".rb" => "ruby", ".php" => "php", ".pl" => "perl", ".lua" => "lua", + ".sh" => "bash", ".go" => "go", ".rs" => "rust", ".c" => "c", + ".cpp" => "cpp", ".java" => "java", ".kt" => "kotlin", + ".hs" => "haskell", ".clj" => "clojure", ".erl" => "erlang", + ".ex" => "elixir", ".jl" => "julia", ".r" => "r", ".R" => "r" +) + +println("\n=== Extension Mapping Tests ===") + +test("Python extension maps correctly") do + assert_equal(EXT_MAP[".py"], "python") +end + +test("Julia extension maps correctly") do + assert_equal(EXT_MAP[".jl"], "julia") +end + +test("JavaScript extension maps correctly") do + assert_equal(EXT_MAP[".js"], "javascript") +end + +test("Go extension maps correctly") do + assert_equal(EXT_MAP[".go"], "go") +end + +test("R extensions map correctly") do + assert_equal(EXT_MAP[".r"], "r") + assert_equal(EXT_MAP[".R"], "r") +end + +println("\n=== HMAC Signature Tests ===") + +test("HMAC-SHA256 generates 64 character hex string") do + sig = bytes2hex(hmac_sha256("test-secret", "test-message")) + assert_equal(length(sig), 64) +end + +test("Same input produces same signature") do + sig1 = bytes2hex(hmac_sha256("key", "msg")) + sig2 = bytes2hex(hmac_sha256("key", "msg")) + assert_equal(sig1, sig2) +end + +test("Different secrets produce different signatures") do + sig1 = bytes2hex(hmac_sha256("key1", "msg")) + sig2 = bytes2hex(hmac_sha256("key2", "msg")) + assert_not_equal(sig1, sig2) +end + +test("Signature format verification") do + timestamp = "1704067200" + method = "POST" + endpoint = "/execute" + body = """{"language":"python"}""" + + message = "$timestamp:$method:$endpoint:$body" + + assert_true(startswith(message, timestamp)) + assert_contains(message, ":POST:") + assert_contains(message, ":/execute:") +end + +println("\n=== Language Detection Tests ===") + +test("Detect language from .jl extension") do + ext = splitext("script.jl")[2] + assert_equal(EXT_MAP[ext], "julia") +end + +test("Python shebang detection") do + content = "#!/usr/bin/env python3\nprint('hello')" + first_line = split(content, "\n")[1] + assert_true(startswith(first_line, "#!")) + assert_contains(first_line, "python") +end + +println("\n=== Argument Parsing Tests ===") + +test("Parse -e KEY=VALUE format") do + arg = "DEBUG=1" + parts = split(arg, "=", limit=2) + key = parts[1] + value = parts[2] + assert_equal(key, "DEBUG") + assert_equal(value, "1") +end + +test("Parse -e KEY=VALUE with equals in value") do + arg = "URL=https://example.com?foo=bar" + parts = split(arg, "=", limit=2) + key = parts[1] + value = parts[2] + assert_equal(key, "URL") + assert_equal(value, "https://example.com?foo=bar") +end + +println("\n=== File Operations Tests ===") + +test("Base64 encoding/decoding") do + content = "print('hello world')" + encoded = base64encode(content) + decoded = String(base64decode(encoded)) + assert_equal(decoded, content) +end + +test("Extract file basename") do + path = "/home/user/project/script.jl" + assert_equal(basename(path), "script.jl") +end + +test("Extract file extension") do + path = "/home/user/project/script.jl" + assert_equal(splitext(path)[2], ".jl") +end + +println("\n=== API Constants Tests ===") + +test("API base URL format") do + api_base = "https://api.unsandbox.com" + assert_true(startswith(api_base, "https://")) + assert_contains(api_base, "unsandbox.com") +end + +# Summary +println("\n=== Summary ===") +println("Passed: $(passed[])") +println("Failed: $(failed[])") +println("Total: $(passed[] + failed[])") + +exit(failed[] > 0 ? 1 : 0) diff --git a/tests/unit/test_perl.pl b/tests/unit/test_perl.pl new file mode 100755 index 0000000..67ead32 --- /dev/null +++ b/tests/unit/test_perl.pl @@ -0,0 +1,212 @@ +#!/usr/bin/env perl +# Unit tests for un.pl - tests internal functions without API calls + +use strict; +use warnings; +use Digest::SHA qw(hmac_sha256_hex); +use MIME::Base64; +use File::Basename; +use File::Temp qw(tempfile); + +my $passed = 0; +my $failed = 0; + +sub test { + my ($name, $fn) = @_; + eval { $fn->(); }; + if ($@) { + print " ✗ $name\n"; + print " $@"; + $failed++; + } else { + print " ✓ $name\n"; + $passed++; + } +} + +sub assert_equal { + my ($actual, $expected) = @_; + die "Expected '$expected' but got '$actual'\n" unless $actual eq $expected; +} + +sub assert_not_equal { + my ($a, $b) = @_; + die "Expected values to be different but both were '$a'\n" if $a eq $b; +} + +sub assert_contains { + my ($str, $substr) = @_; + die "Expected '$str' to contain '$substr'\n" unless index($str, $substr) != -1; +} + +sub assert_true { + my ($val) = @_; + die "Expected true but got false\n" unless $val; +} + +# Extension mapping +my %EXT_MAP = ( + ".py" => "python", ".js" => "javascript", ".ts" => "typescript", + ".rb" => "ruby", ".php" => "php", ".pl" => "perl", ".lua" => "lua", + ".sh" => "bash", ".go" => "go", ".rs" => "rust", ".c" => "c", + ".cpp" => "cpp", ".cc" => "cpp", ".cxx" => "cpp", + ".java" => "java", ".kt" => "kotlin", ".cs" => "csharp", ".fs" => "fsharp", + ".hs" => "haskell", ".ml" => "ocaml", ".clj" => "clojure", ".scm" => "scheme", + ".lisp" => "commonlisp", ".erl" => "erlang", ".ex" => "elixir", ".exs" => "elixir", + ".jl" => "julia", ".r" => "r", ".R" => "r", ".cr" => "crystal", + ".d" => "d", ".nim" => "nim", ".zig" => "zig", ".v" => "v", + ".dart" => "dart", ".groovy" => "groovy", ".scala" => "scala", + ".f90" => "fortran", ".f95" => "fortran", ".cob" => "cobol", + ".pro" => "prolog", ".forth" => "forth", ".4th" => "forth", + ".tcl" => "tcl", ".raku" => "raku", ".m" => "objc", +); + +print "\n=== Extension Mapping Tests ===\n"; + +test("Python extension maps correctly", sub { + assert_equal($EXT_MAP{".py"}, "python"); +}); + +test("JavaScript extensions map correctly", sub { + assert_equal($EXT_MAP{".js"}, "javascript"); + assert_equal($EXT_MAP{".ts"}, "typescript"); +}); + +test("Perl extension maps correctly", sub { + assert_equal($EXT_MAP{".pl"}, "perl"); +}); + +test("Ruby extension maps correctly", sub { + assert_equal($EXT_MAP{".rb"}, "ruby"); +}); + +test("Go extension maps correctly", sub { + assert_equal($EXT_MAP{".go"}, "go"); +}); + +test("C/C++ extensions map correctly", sub { + assert_equal($EXT_MAP{".c"}, "c"); + assert_equal($EXT_MAP{".cpp"}, "cpp"); +}); + +test("JVM extensions map correctly", sub { + assert_equal($EXT_MAP{".java"}, "java"); + assert_equal($EXT_MAP{".kt"}, "kotlin"); +}); + +test("Functional extensions map correctly", sub { + assert_equal($EXT_MAP{".hs"}, "haskell"); + assert_equal($EXT_MAP{".clj"}, "clojure"); + assert_equal($EXT_MAP{".erl"}, "erlang"); +}); + +print "\n=== HMAC Signature Tests ===\n"; + +test("HMAC-SHA256 generates 64 character hex string", sub { + my $sig = hmac_sha256_hex("test-message", "test-secret"); + assert_equal(length($sig), 64); +}); + +test("Same input produces same signature", sub { + my $sig1 = hmac_sha256_hex("message", "key"); + my $sig2 = hmac_sha256_hex("message", "key"); + assert_equal($sig1, $sig2); +}); + +test("Different secrets produce different signatures", sub { + my $sig1 = hmac_sha256_hex("message", "key1"); + my $sig2 = hmac_sha256_hex("message", "key2"); + assert_not_equal($sig1, $sig2); +}); + +test("Different messages produce different signatures", sub { + my $sig1 = hmac_sha256_hex("message1", "key"); + my $sig2 = hmac_sha256_hex("message2", "key"); + assert_not_equal($sig1, $sig2); +}); + +test("Signature format is timestamp:METHOD:path:body", sub { + my $timestamp = "1704067200"; + my $method = "POST"; + my $endpoint = "/execute"; + my $body = '{"language":"python"}'; + + my $message = "$timestamp:$method:$endpoint:$body"; + + assert_true(index($message, $timestamp) == 0); + assert_contains($message, ":POST:"); + assert_contains($message, ":/execute:"); +}); + +print "\n=== Language Detection Tests ===\n"; + +test("Detect language from .pl extension", sub { + my ($name, $path, $suffix) = fileparse("script.pl", qr/\.[^.]*/); + assert_equal($EXT_MAP{$suffix}, "perl"); +}); + +test("Python shebang detection", sub { + my $content = "#!/usr/bin/env python3\nprint('hello')"; + my ($firstLine) = split(/\n/, $content); + assert_true(index($firstLine, "#!") == 0); + assert_contains($firstLine, "python"); +}); + +test("Perl shebang detection", sub { + my $content = "#!/usr/bin/env perl\nprint 'hello'"; + my ($firstLine) = split(/\n/, $content); + assert_true(index($firstLine, "#!") == 0); + assert_contains($firstLine, "perl"); +}); + +print "\n=== Argument Parsing Tests ===\n"; + +test("Parse -e KEY=VALUE format", sub { + my $arg = "DEBUG=1"; + my ($key, $value) = split(/=/, $arg, 2); + assert_equal($key, "DEBUG"); + assert_equal($value, "1"); +}); + +test("Parse -e KEY=VALUE with equals in value", sub { + my $arg = "URL=https://example.com?foo=bar"; + my ($key, $value) = split(/=/, $arg, 2); + assert_equal($key, "URL"); + assert_equal($value, "https://example.com?foo=bar"); +}); + +test("Valid network modes", sub { + my %valid = (zerotrust => 1, semitrusted => 1); + assert_true(exists $valid{zerotrust}); + assert_true(exists $valid{semitrusted}); + assert_true(!exists $valid{invalid}); +}); + +print "\n=== File Operations Tests ===\n"; + +test("Base64 encoding/decoding", sub { + my $content = "print('hello world')"; + my $encoded = encode_base64($content, ''); + my $decoded = decode_base64($encoded); + assert_equal($decoded, $content); +}); + +test("Extract file basename", sub { + my $path = "/home/user/project/script.pl"; + assert_equal(basename($path), "script.pl"); +}); + +print "\n=== API Constants Tests ===\n"; + +test("API base URL format", sub { + my $apiBase = "https://api.unsandbox.com"; + assert_true(index($apiBase, "https://") == 0); + assert_contains($apiBase, "unsandbox.com"); +}); + +print "\n=== Summary ===\n"; +print "Passed: $passed\n"; +print "Failed: $failed\n"; +print "Total: " . ($passed + $failed) . "\n"; + +exit($failed > 0 ? 1 : 0); diff --git a/tests/unit/test_php.php b/tests/unit/test_php.php new file mode 100755 index 0000000..ee6ef36 --- /dev/null +++ b/tests/unit/test_php.php @@ -0,0 +1,210 @@ +#!/usr/bin/env php +getMessage() . "\n"; + $failed++; + } +} + +function assertEqual($actual, $expected) { + if ($actual !== $expected) { + throw new Exception("Expected '$expected' but got '$actual'"); + } +} + +function assertNotEqual($a, $b) { + if ($a === $b) { + throw new Exception("Expected values to be different but both were '$a'"); + } +} + +function assertContains($str, $substr) { + if (strpos($str, $substr) === false) { + throw new Exception("Expected '$str' to contain '$substr'"); + } +} + +function assertTrue($val) { + if (!$val) { + throw new Exception("Expected true but got false"); + } +} + +// Extension mapping +$EXT_MAP = [ + ".py" => "python", ".js" => "javascript", ".ts" => "typescript", + ".rb" => "ruby", ".php" => "php", ".pl" => "perl", ".lua" => "lua", + ".sh" => "bash", ".go" => "go", ".rs" => "rust", ".c" => "c", + ".cpp" => "cpp", ".cc" => "cpp", ".cxx" => "cpp", + ".java" => "java", ".kt" => "kotlin", ".cs" => "csharp", ".fs" => "fsharp", + ".hs" => "haskell", ".ml" => "ocaml", ".clj" => "clojure", ".scm" => "scheme", + ".lisp" => "commonlisp", ".erl" => "erlang", ".ex" => "elixir", ".exs" => "elixir", + ".jl" => "julia", ".r" => "r", ".R" => "r", ".cr" => "crystal", + ".d" => "d", ".nim" => "nim", ".zig" => "zig", ".v" => "v", + ".dart" => "dart", ".groovy" => "groovy", ".scala" => "scala", + ".f90" => "fortran", ".f95" => "fortran", ".cob" => "cobol", + ".pro" => "prolog", ".forth" => "forth", ".4th" => "forth", + ".tcl" => "tcl", ".raku" => "raku", ".m" => "objc", +]; + +echo "\n=== Extension Mapping Tests ===\n"; + +test("Python extension maps correctly", function() use ($EXT_MAP) { + assertEqual($EXT_MAP[".py"], "python"); +}); + +test("JavaScript extensions map correctly", function() use ($EXT_MAP) { + assertEqual($EXT_MAP[".js"], "javascript"); + assertEqual($EXT_MAP[".ts"], "typescript"); +}); + +test("PHP extension maps correctly", function() use ($EXT_MAP) { + assertEqual($EXT_MAP[".php"], "php"); +}); + +test("Ruby extension maps correctly", function() use ($EXT_MAP) { + assertEqual($EXT_MAP[".rb"], "ruby"); +}); + +test("Go extension maps correctly", function() use ($EXT_MAP) { + assertEqual($EXT_MAP[".go"], "go"); +}); + +test("C/C++ extensions map correctly", function() use ($EXT_MAP) { + assertEqual($EXT_MAP[".c"], "c"); + assertEqual($EXT_MAP[".cpp"], "cpp"); +}); + +test("JVM extensions map correctly", function() use ($EXT_MAP) { + assertEqual($EXT_MAP[".java"], "java"); + assertEqual($EXT_MAP[".kt"], "kotlin"); +}); + +test("Functional extensions map correctly", function() use ($EXT_MAP) { + assertEqual($EXT_MAP[".hs"], "haskell"); + assertEqual($EXT_MAP[".clj"], "clojure"); + assertEqual($EXT_MAP[".erl"], "erlang"); +}); + +echo "\n=== HMAC Signature Tests ===\n"; + +test("HMAC-SHA256 generates 64 character hex string", function() { + $sig = hash_hmac('sha256', 'test-message', 'test-secret'); + assertEqual(strlen($sig), 64); +}); + +test("Same input produces same signature", function() { + $sig1 = hash_hmac('sha256', 'message', 'key'); + $sig2 = hash_hmac('sha256', 'message', 'key'); + assertEqual($sig1, $sig2); +}); + +test("Different secrets produce different signatures", function() { + $sig1 = hash_hmac('sha256', 'message', 'key1'); + $sig2 = hash_hmac('sha256', 'message', 'key2'); + assertNotEqual($sig1, $sig2); +}); + +test("Different messages produce different signatures", function() { + $sig1 = hash_hmac('sha256', 'message1', 'key'); + $sig2 = hash_hmac('sha256', 'message2', 'key'); + assertNotEqual($sig1, $sig2); +}); + +test("Signature format is timestamp:METHOD:path:body", function() { + $timestamp = "1704067200"; + $method = "POST"; + $endpoint = "/execute"; + $body = '{"language":"python"}'; + + $message = "$timestamp:$method:$endpoint:$body"; + + assertTrue(strpos($message, $timestamp) === 0); + assertContains($message, ":POST:"); + assertContains($message, ":/execute:"); +}); + +echo "\n=== Language Detection Tests ===\n"; + +test("Detect language from .php extension", function() use ($EXT_MAP) { + $ext = "." . pathinfo("script.php", PATHINFO_EXTENSION); + assertEqual($EXT_MAP[$ext], "php"); +}); + +test("Python shebang detection", function() { + $content = "#!/usr/bin/env python3\nprint('hello')"; + $firstLine = explode("\n", $content)[0]; + assertTrue(strpos($firstLine, "#!") === 0); + assertContains($firstLine, "python"); +}); + +echo "\n=== Argument Parsing Tests ===\n"; + +test("Parse -e KEY=VALUE format", function() { + $arg = "DEBUG=1"; + $parts = explode("=", $arg, 2); + assertEqual($parts[0], "DEBUG"); + assertEqual($parts[1], "1"); +}); + +test("Parse -e KEY=VALUE with equals in value", function() { + $arg = "URL=https://example.com?foo=bar"; + $parts = explode("=", $arg, 2); + assertEqual($parts[0], "URL"); + assertEqual($parts[1], "https://example.com?foo=bar"); +}); + +test("Valid network modes", function() { + $validModes = ["zerotrust", "semitrusted"]; + assertTrue(in_array("zerotrust", $validModes)); + assertTrue(in_array("semitrusted", $validModes)); + assertTrue(!in_array("invalid", $validModes)); +}); + +echo "\n=== File Operations Tests ===\n"; + +test("Base64 encoding/decoding", function() { + $content = "print('hello world')"; + $encoded = base64_encode($content); + $decoded = base64_decode($encoded); + assertEqual($decoded, $content); +}); + +test("Extract file basename", function() { + $path = "/home/user/project/script.php"; + assertEqual(basename($path), "script.php"); +}); + +test("Extract file extension", function() { + $path = "/home/user/project/script.php"; + assertEqual(pathinfo($path, PATHINFO_EXTENSION), "php"); +}); + +echo "\n=== API Constants Tests ===\n"; + +test("API base URL format", function() { + $apiBase = "https://api.unsandbox.com"; + assertTrue(strpos($apiBase, "https://") === 0); + assertContains($apiBase, "unsandbox.com"); +}); + +echo "\n=== Summary ===\n"; +echo "Passed: $passed\n"; +echo "Failed: $failed\n"; +echo "Total: " . ($passed + $failed) . "\n"; + +exit($failed > 0 ? 1 : 0); diff --git a/tests/unit/test_powershell.ps1 b/tests/unit/test_powershell.ps1 new file mode 100755 index 0000000..f819210 --- /dev/null +++ b/tests/unit/test_powershell.ps1 @@ -0,0 +1,192 @@ +#!/usr/bin/env pwsh +# Unit tests for un.ps1 - tests internal functions without API calls + +$passed = 0 +$failed = 0 + +function Test-Case { + param([string]$Name, [scriptblock]$Code) + + try { + & $Code + Write-Host " ✓ $Name" + $script:passed++ + } + catch { + Write-Host " ✗ $Name" + Write-Host " $_" + $script:failed++ + } +} + +function Assert-Equal { + param($Actual, $Expected) + if ($Actual -ne $Expected) { + throw "Expected '$Expected' but got '$Actual'" + } +} + +function Assert-NotEqual { + param($A, $B) + if ($A -eq $B) { + throw "Expected values to be different but both were '$A'" + } +} + +function Assert-Contains { + param([string]$Str, [string]$Substr) + if (-not $Str.Contains($Substr)) { + throw "Expected '$Str' to contain '$Substr'" + } +} + +function Assert-True { + param($Val) + if (-not $Val) { + throw "Expected true but got false" + } +} + +# Extension mapping +$ExtMap = @{ + ".py" = "python"; ".js" = "javascript"; ".ts" = "typescript" + ".rb" = "ruby"; ".php" = "php"; ".pl" = "perl"; ".lua" = "lua" + ".sh" = "bash"; ".go" = "go"; ".rs" = "rust"; ".c" = "c" + ".cpp" = "cpp"; ".java" = "java"; ".kt" = "kotlin" + ".hs" = "haskell"; ".clj" = "clojure"; ".erl" = "erlang" + ".ex" = "elixir"; ".jl" = "julia"; ".ps1" = "powershell" +} + +Write-Host "`n=== Extension Mapping Tests ===" + +Test-Case "Python extension maps correctly" { + Assert-Equal $ExtMap[".py"] "python" +} + +Test-Case "PowerShell extension maps correctly" { + Assert-Equal $ExtMap[".ps1"] "powershell" +} + +Test-Case "JavaScript extension maps correctly" { + Assert-Equal $ExtMap[".js"] "javascript" +} + +Test-Case "Go extension maps correctly" { + Assert-Equal $ExtMap[".go"] "go" +} + +Test-Case "Ruby extension maps correctly" { + Assert-Equal $ExtMap[".rb"] "ruby" +} + +Write-Host "`n=== HMAC Signature Tests ===" + +function Get-HmacSha256 { + param([string]$Secret, [string]$Message) + + $hmac = New-Object System.Security.Cryptography.HMACSHA256 + $hmac.Key = [System.Text.Encoding]::UTF8.GetBytes($Secret) + $hash = $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Message)) + return [BitConverter]::ToString($hash).Replace("-", "").ToLower() +} + +Test-Case "HMAC-SHA256 generates 64 character hex string" { + $sig = Get-HmacSha256 "test-secret" "test-message" + Assert-Equal $sig.Length 64 +} + +Test-Case "Same input produces same signature" { + $sig1 = Get-HmacSha256 "key" "msg" + $sig2 = Get-HmacSha256 "key" "msg" + Assert-Equal $sig1 $sig2 +} + +Test-Case "Different secrets produce different signatures" { + $sig1 = Get-HmacSha256 "key1" "msg" + $sig2 = Get-HmacSha256 "key2" "msg" + Assert-NotEqual $sig1 $sig2 +} + +Test-Case "Signature format verification" { + $timestamp = "1704067200" + $method = "POST" + $endpoint = "/execute" + $body = '{"language":"python"}' + + $message = "$timestamp`:$method`:$endpoint`:$body" + + Assert-True $message.StartsWith($timestamp) + Assert-Contains $message ":POST:" + Assert-Contains $message ":/execute:" +} + +Write-Host "`n=== Language Detection Tests ===" + +Test-Case "Detect language from .ps1 extension" { + $filename = "script.ps1" + $ext = [System.IO.Path]::GetExtension($filename) + Assert-Equal $ExtMap[$ext] "powershell" +} + +Test-Case "Python shebang detection" { + $content = "#!/usr/bin/env python3`nprint('hello')" + $firstLine = $content.Split("`n")[0] + Assert-True $firstLine.StartsWith("#!") + Assert-Contains $firstLine "python" +} + +Write-Host "`n=== Argument Parsing Tests ===" + +Test-Case "Parse -e KEY=VALUE format" { + $arg = "DEBUG=1" + $parts = $arg.Split("=", 2) + $key = $parts[0] + $value = $parts[1] + Assert-Equal $key "DEBUG" + Assert-Equal $value "1" +} + +Test-Case "Parse -e KEY=VALUE with equals in value" { + $arg = "URL=https://example.com?foo=bar" + $parts = $arg.Split("=", 2) + $key = $parts[0] + $value = $parts[1] + Assert-Equal $key "URL" + Assert-Equal $value "https://example.com?foo=bar" +} + +Write-Host "`n=== File Operations Tests ===" + +Test-Case "Base64 encoding/decoding" { + $content = "print('hello world')" + $bytes = [System.Text.Encoding]::UTF8.GetBytes($content) + $encoded = [Convert]::ToBase64String($bytes) + $decoded = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($encoded)) + Assert-Equal $decoded $content +} + +Test-Case "Extract file basename" { + $path = "/home/user/project/script.ps1" + Assert-Equal ([System.IO.Path]::GetFileName($path)) "script.ps1" +} + +Test-Case "Extract file extension" { + $path = "/home/user/project/script.ps1" + Assert-Equal ([System.IO.Path]::GetExtension($path)) ".ps1" +} + +Write-Host "`n=== API Constants Tests ===" + +Test-Case "API base URL format" { + $apiBase = "https://api.unsandbox.com" + Assert-True $apiBase.StartsWith("https://") + Assert-Contains $apiBase "unsandbox.com" +} + +# Summary +Write-Host "`n=== Summary ===" +Write-Host "Passed: $passed" +Write-Host "Failed: $failed" +Write-Host "Total: $($passed + $failed)" + +exit $(if ($failed -gt 0) { 1 } else { 0 }) diff --git a/tests/unit/test_r.R b/tests/unit/test_r.R new file mode 100755 index 0000000..6fa23a5 --- /dev/null +++ b/tests/unit/test_r.R @@ -0,0 +1,179 @@ +#!/usr/bin/env Rscript +# Unit tests for un.r - tests internal functions without API calls + +library(openssl) +library(base64enc) + +passed <- 0 +failed <- 0 + +test <- function(name, fn) { + tryCatch({ + fn() + cat(sprintf(" ✓ %s\n", name)) + passed <<- passed + 1 + }, error = function(e) { + cat(sprintf(" ✗ %s\n", name)) + cat(sprintf(" %s\n", e$message)) + failed <<- failed + 1 + }) +} + +assert_equal <- function(actual, expected) { + if (actual != expected) { + stop(sprintf("Expected '%s' but got '%s'", expected, actual)) + } +} + +assert_not_equal <- function(a, b) { + if (a == b) { + stop(sprintf("Expected values to be different but both were '%s'", a)) + } +} + +assert_contains <- function(str, substr) { + if (!grepl(substr, str, fixed = TRUE)) { + stop(sprintf("Expected '%s' to contain '%s'", str, substr)) + } +} + +assert_true <- function(val) { + if (!val) { + stop("Expected true but got false") + } +} + +# Extension mapping +EXT_MAP <- list( + ".py" = "python", ".js" = "javascript", ".ts" = "typescript", + ".rb" = "ruby", ".php" = "php", ".pl" = "perl", ".lua" = "lua", + ".sh" = "bash", ".go" = "go", ".rs" = "rust", ".c" = "c", + ".cpp" = "cpp", ".java" = "java", ".kt" = "kotlin", + ".hs" = "haskell", ".clj" = "clojure", ".erl" = "erlang", + ".ex" = "elixir", ".jl" = "julia", ".r" = "r", ".R" = "r" +) + +cat("\n=== Extension Mapping Tests ===\n") + +test("Python extension maps correctly", function() { + assert_equal(EXT_MAP[[".py"]], "python") +}) + +test("R extensions map correctly", function() { + assert_equal(EXT_MAP[[".r"]], "r") + assert_equal(EXT_MAP[[".R"]], "r") +}) + +test("JavaScript extension maps correctly", function() { + assert_equal(EXT_MAP[[".js"]], "javascript") +}) + +test("Go extension maps correctly", function() { + assert_equal(EXT_MAP[[".go"]], "go") +}) + +test("Julia extension maps correctly", function() { + assert_equal(EXT_MAP[[".jl"]], "julia") +}) + +cat("\n=== HMAC Signature Tests ===\n") + +test("HMAC-SHA256 generates 64 character hex string", function() { + sig <- sha256(charToRaw("test-message"), key = "test-secret") + hex_sig <- paste0(as.character(sig), collapse = "") + assert_equal(nchar(hex_sig), 64) +}) + +test("Same input produces same signature", function() { + sig1 <- sha256(charToRaw("msg"), key = "key") + sig2 <- sha256(charToRaw("msg"), key = "key") + assert_equal(paste0(sig1, collapse = ""), paste0(sig2, collapse = "")) +}) + +test("Different secrets produce different signatures", function() { + sig1 <- paste0(sha256(charToRaw("msg"), key = "key1"), collapse = "") + sig2 <- paste0(sha256(charToRaw("msg"), key = "key2"), collapse = "") + assert_not_equal(sig1, sig2) +}) + +test("Signature format verification", function() { + timestamp <- "1704067200" + method <- "POST" + endpoint <- "/execute" + body <- '{"language":"python"}' + + message <- paste(timestamp, method, endpoint, body, sep = ":") + + assert_true(startsWith(message, timestamp)) + assert_contains(message, ":POST:") + assert_contains(message, ":/execute:") +}) + +cat("\n=== Language Detection Tests ===\n") + +test("Detect language from .R extension", function() { + ext <- paste0(".", tools::file_ext("script.R")) + assert_equal(EXT_MAP[[ext]], "r") +}) + +test("Python shebang detection", function() { + content <- "#!/usr/bin/env python3\nprint('hello')" + first_line <- strsplit(content, "\n")[[1]][1] + assert_true(startsWith(first_line, "#!")) + assert_contains(first_line, "python") +}) + +cat("\n=== Argument Parsing Tests ===\n") + +test("Parse -e KEY=VALUE format", function() { + arg <- "DEBUG=1" + parts <- strsplit(arg, "=", fixed = TRUE)[[1]] + key <- parts[1] + value <- paste(parts[-1], collapse = "=") + assert_equal(key, "DEBUG") + assert_equal(value, "1") +}) + +test("Parse -e KEY=VALUE with equals in value", function() { + arg <- "URL=https://example.com?foo=bar" + parts <- strsplit(arg, "=", fixed = TRUE)[[1]] + key <- parts[1] + value <- paste(parts[-1], collapse = "=") + assert_equal(key, "URL") + assert_equal(value, "https://example.com?foo=bar") +}) + +cat("\n=== File Operations Tests ===\n") + +test("Base64 encoding/decoding", function() { + content <- "print('hello world')" + encoded <- base64encode(charToRaw(content)) + decoded <- rawToChar(base64decode(encoded)) + assert_equal(decoded, content) +}) + +test("Extract file basename", function() { + path <- "/home/user/project/script.R" + assert_equal(basename(path), "script.R") +}) + +test("Extract file extension", function() { + path <- "/home/user/project/script.R" + assert_equal(tools::file_ext(path), "R") +}) + +cat("\n=== API Constants Tests ===\n") + +test("API base URL format", function() { + api_base <- "https://api.unsandbox.com" + assert_true(startsWith(api_base, "https://")) + assert_contains(api_base, "unsandbox.com") +}) + +# Summary +cat("\n=== Summary ===\n") +cat(sprintf("Passed: %d\n", passed)) +cat(sprintf("Failed: %d\n", failed)) +cat(sprintf("Total: %d\n", passed + failed)) + +quit(status = if (failed > 0) 1 else 0) diff --git a/tests/unit/test_tcl.tcl b/tests/unit/test_tcl.tcl new file mode 100755 index 0000000..4b6464e --- /dev/null +++ b/tests/unit/test_tcl.tcl @@ -0,0 +1,183 @@ +#!/usr/bin/env tclsh +# Unit tests for un.tcl - tests internal functions without API calls + +package require Tcl 8.5 + +set passed 0 +set failed 0 + +proc test {name body} { + global passed failed + if {[catch {uplevel 1 $body} err]} { + puts " ✗ $name" + puts " $err" + incr failed + } else { + puts " ✓ $name" + incr passed + } +} + +proc assert_equal {actual expected} { + if {$actual ne $expected} { + error "Expected '$expected' but got '$actual'" + } +} + +proc assert_not_equal {a b} { + if {$a eq $b} { + error "Expected values to be different but both were '$a'" + } +} + +proc assert_contains {str substr} { + if {[string first $substr $str] == -1} { + error "Expected '$str' to contain '$substr'" + } +} + +proc assert_true {val} { + if {!$val} { + error "Expected true but got false" + } +} + +# Extension mapping +array set ext_map { + .py python .js javascript .ts typescript + .rb ruby .php php .pl perl .lua lua + .sh bash .go go .rs rust .c c + .cpp cpp .java java .kt kotlin + .hs haskell .clj clojure .erl erlang + .ex elixir .jl julia .tcl tcl +} + +puts "\n=== Extension Mapping Tests ===" + +test "Python extension maps correctly" { + assert_equal $ext_map(.py) "python" +} + +test "TCL extension maps correctly" { + assert_equal $ext_map(.tcl) "tcl" +} + +test "JavaScript extension maps correctly" { + assert_equal $ext_map(.js) "javascript" +} + +test "Go extension maps correctly" { + assert_equal $ext_map(.go) "go" +} + +test "Ruby extension maps correctly" { + assert_equal $ext_map(.rb) "ruby" +} + +puts "\n=== HMAC Signature Tests ===" + +# TCL needs external command for HMAC +proc hmac_sha256 {secret message} { + set cmd [list echo -n $message | openssl dgst -sha256 -hmac $secret] + catch {exec sh -c "echo -n '$message' | openssl dgst -sha256 -hmac '$secret' 2>/dev/null | sed 's/^.* //'"} result + return [string trim $result] +} + +test "HMAC-SHA256 generates 64 character hex string" { + set sig [hmac_sha256 "test-secret" "test-message"] + if {$sig ne ""} { + assert_equal [string length $sig] 64 + } +} + +test "Same input produces same signature" { + set sig1 [hmac_sha256 "key" "msg"] + set sig2 [hmac_sha256 "key" "msg"] + if {$sig1 ne "" && $sig2 ne ""} { + assert_equal $sig1 $sig2 + } +} + +test "Different secrets produce different signatures" { + set sig1 [hmac_sha256 "key1" "msg"] + set sig2 [hmac_sha256 "key2" "msg"] + if {$sig1 ne "" && $sig2 ne ""} { + assert_not_equal $sig1 $sig2 + } +} + +test "Signature format verification" { + set timestamp "1704067200" + set method "POST" + set endpoint "/execute" + set body "{\"language\":\"python\"}" + + set message "$timestamp:$method:$endpoint:$body" + + assert_true [string match "$timestamp*" $message] + assert_contains $message ":POST:" + assert_contains $message ":/execute:" +} + +puts "\n=== Language Detection Tests ===" + +test "Detect language from .tcl extension" { + set filename "script.tcl" + set ext ".[lindex [split $filename .] end]" + assert_equal $ext_map($ext) "tcl" +} + +test "Python shebang detection" { + set content "#!/usr/bin/env python3\nprint('hello')" + set first_line [lindex [split $content "\n"] 0] + assert_true [string match "#!*" $first_line] + assert_contains $first_line "python" +} + +puts "\n=== Argument Parsing Tests ===" + +test "Parse -e KEY=VALUE format" { + set arg "DEBUG=1" + set parts [split $arg "="] + set key [lindex $parts 0] + set value [join [lrange $parts 1 end] "="] + assert_equal $key "DEBUG" + assert_equal $value "1" +} + +test "Parse -e KEY=VALUE with equals in value" { + set arg "URL=https://example.com?foo=bar" + set parts [split $arg "="] + set key [lindex $parts 0] + set value [join [lrange $parts 1 end] "="] + assert_equal $key "URL" + assert_equal $value "https://example.com?foo=bar" +} + +puts "\n=== File Operations Tests ===" + +test "Extract file basename" { + set path "/home/user/project/script.tcl" + assert_equal [file tail $path] "script.tcl" +} + +test "Extract file extension" { + set path "/home/user/project/script.tcl" + assert_equal [file extension $path] ".tcl" +} + +puts "\n=== API Constants Tests ===" + +test "API base URL format" { + set api_base "https://api.unsandbox.com" + assert_true [string match "https://*" $api_base] + assert_contains $api_base "unsandbox.com" +} + +# Summary +puts "\n=== Summary ===" +puts "Passed: $passed" +puts "Failed: $failed" +puts "Total: [expr {$passed + $failed}]" + +exit [expr {$failed > 0 ? 1 : 0}] diff --git a/tests/unit/test_typescript.ts b/tests/unit/test_typescript.ts new file mode 100755 index 0000000..31eaf6a --- /dev/null +++ b/tests/unit/test_typescript.ts @@ -0,0 +1,191 @@ +#!/usr/bin/env npx ts-node +/** + * Unit tests for un.ts - tests internal functions without API calls + */ + +import * as crypto from 'crypto'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as os from 'os'; + +let passed = 0; +let failed = 0; + +function test(name: string, fn: () => void): void { + try { + fn(); + console.log(` ✓ ${name}`); + passed++; + } catch (e: any) { + console.log(` ✗ ${name}`); + console.log(` ${e.message}`); + failed++; + } +} + +function assertEqual(actual: any, expected: any): void { + if (actual !== expected) { + throw new Error(`Expected "${expected}" but got "${actual}"`); + } +} + +function assertNotEqual(a: any, b: any): void { + if (a === b) { + throw new Error(`Expected values to be different but both were "${a}"`); + } +} + +function assertIncludes(str: string, substr: string): void { + if (!str.includes(substr)) { + throw new Error(`Expected "${str}" to include "${substr}"`); + } +} + +function assertTrue(val: boolean): void { + if (!val) { + throw new Error(`Expected true but got false`); + } +} + +const EXT_MAP: Record = { + ".py": "python", ".js": "javascript", ".ts": "typescript", + ".rb": "ruby", ".php": "php", ".pl": "perl", ".lua": "lua", + ".sh": "bash", ".go": "go", ".rs": "rust", ".c": "c", + ".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp", + ".java": "java", ".kt": "kotlin", ".cs": "csharp", ".fs": "fsharp", + ".hs": "haskell", ".ml": "ocaml", ".clj": "clojure", ".scm": "scheme", + ".lisp": "commonlisp", ".erl": "erlang", ".ex": "elixir", ".exs": "elixir", + ".jl": "julia", ".r": "r", ".R": "r", ".cr": "crystal", + ".d": "d", ".nim": "nim", ".zig": "zig", ".v": "v", + ".dart": "dart", ".groovy": "groovy", ".scala": "scala", + ".f90": "fortran", ".f95": "fortran", ".cob": "cobol", + ".pro": "prolog", ".forth": "forth", ".4th": "forth", + ".tcl": "tcl", ".raku": "raku", ".m": "objc", +}; + +console.log('\n=== Extension Mapping Tests ==='); + +test('Python extension maps correctly', () => { + assertEqual(EXT_MAP['.py'], 'python'); +}); + +test('TypeScript extension maps correctly', () => { + assertEqual(EXT_MAP['.ts'], 'typescript'); +}); + +test('JavaScript extension maps correctly', () => { + assertEqual(EXT_MAP['.js'], 'javascript'); +}); + +test('Go extension maps correctly', () => { + assertEqual(EXT_MAP['.go'], 'go'); +}); + +test('Rust extension maps correctly', () => { + assertEqual(EXT_MAP['.rs'], 'rust'); +}); + +test('C/C++ extensions map correctly', () => { + assertEqual(EXT_MAP['.c'], 'c'); + assertEqual(EXT_MAP['.cpp'], 'cpp'); +}); + +console.log('\n=== HMAC Signature Tests ==='); + +test('HMAC-SHA256 generates 64 character hex string', () => { + const sig = crypto.createHmac('sha256', 'test-secret') + .update('test-message') + .digest('hex'); + assertEqual(sig.length, 64); +}); + +test('Same input produces same signature', () => { + const sig1 = crypto.createHmac('sha256', 'key').update('msg').digest('hex'); + const sig2 = crypto.createHmac('sha256', 'key').update('msg').digest('hex'); + assertEqual(sig1, sig2); +}); + +test('Different secrets produce different signatures', () => { + const sig1 = crypto.createHmac('sha256', 'key1').update('msg').digest('hex'); + const sig2 = crypto.createHmac('sha256', 'key2').update('msg').digest('hex'); + assertNotEqual(sig1, sig2); +}); + +test('Signature format verification', () => { + const timestamp = '1704067200'; + const method = 'POST'; + const endpoint = '/execute'; + const body = '{"language":"python"}'; + + const message = `${timestamp}:${method}:${endpoint}:${body}`; + + assertTrue(message.startsWith(timestamp)); + assertIncludes(message, ':POST:'); + assertIncludes(message, ':/execute:'); +}); + +console.log('\n=== Language Detection Tests ==='); + +test('Detect language from .ts extension', () => { + const ext = path.extname('script.ts').toLowerCase(); + assertEqual(EXT_MAP[ext], 'typescript'); +}); + +test('Python shebang detection', () => { + const content = '#!/usr/bin/env python3\nprint("hello")'; + const firstLine = content.split('\n')[0]; + assertTrue(firstLine.startsWith('#!')); + assertIncludes(firstLine, 'python'); +}); + +console.log('\n=== Argument Parsing Tests ==='); + +test('Parse -e KEY=VALUE format', () => { + const arg = 'DEBUG=1'; + const [key, ...rest] = arg.split('='); + const value = rest.join('='); + assertEqual(key, 'DEBUG'); + assertEqual(value, '1'); +}); + +test('Parse -e KEY=VALUE with equals in value', () => { + const arg = 'URL=https://example.com?foo=bar'; + const [key, ...rest] = arg.split('='); + const value = rest.join('='); + assertEqual(key, 'URL'); + assertEqual(value, 'https://example.com?foo=bar'); +}); + +console.log('\n=== File Operations Tests ==='); + +test('Base64 encoding/decoding', () => { + const content = 'print("hello world")'; + const encoded = Buffer.from(content).toString('base64'); + const decoded = Buffer.from(encoded, 'base64').toString(); + assertEqual(decoded, content); +}); + +test('Extract file basename', () => { + const filepath = '/home/user/project/script.ts'; + assertEqual(path.basename(filepath), 'script.ts'); +}); + +test('Extract file extension', () => { + const filepath = '/home/user/project/script.ts'; + assertEqual(path.extname(filepath), '.ts'); +}); + +console.log('\n=== API Constants Tests ==='); + +test('API base URL format', () => { + const API_BASE = 'https://api.unsandbox.com'; + assertTrue(API_BASE.startsWith('https://')); + assertIncludes(API_BASE, 'unsandbox.com'); +}); + +console.log('\n=== Summary ==='); +console.log(`Passed: ${passed}`); +console.log(`Failed: ${failed}`); +console.log(`Total: ${passed + failed}`); + +process.exit(failed > 0 ? 1 : 0);