diff --git a/.github/actions/report-pr/action.yml b/.github/actions/report-pr/action.yml index acb377d..a01e3e7 100644 --- a/.github/actions/report-pr/action.yml +++ b/.github/actions/report-pr/action.yml @@ -18,7 +18,7 @@ runs: badge: true fail_below_min: false format: markdown - hide_branch_rate: false + hide_branch_rate: true hide_complexity: true indicators: true output: both diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 6da8729..00a2959 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,6 +1,14 @@ name: Test PR "on": pull_request +permissions: + contents: read + pull-requests: write + +env: + # kcov/kcov:latest, pinned for reproducible builds + KCOV_IMAGE: kcov/kcov@sha256:481289ae32e55e5b733019515acd10948a4f76dfed381765577db909664fc603 + jobs: install-and-test: name: ๐Ÿ› ๏ธ Install & Test @@ -30,6 +38,27 @@ jobs: - name: โœ… Validate run: yarn validate + - name: ๐Ÿ› ๏ธ Build coverage tests + run: yarn coverage:build + + - name: ๐Ÿงช Generate coverage + run: | + docker run --rm \ + --cap-add=SYS_PTRACE \ + --security-opt seccomp=unconfined \ + --volume "$PWD:$PWD" \ + --workdir "$PWD" \ + --user "$(id -u):$(id -g)" \ + --entrypoint /bin/sh \ + "$KCOV_IMAGE" \ + "$PWD/scripts/coverage.sh" + + - name: ๐Ÿ“Š Report coverage + if: github.event.pull_request.head.repo.full_name == github.repository + uses: ./.github/actions/report-pr + with: + coverage-file: coverage/merged/kcov-merged/cobertura.xml + - name: ๐Ÿ› ๏ธ Setup package run: yarn script:package-npm diff --git a/.gitignore b/.gitignore index 2e2a086..bc8f9e3 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ Thumbs.db # Temp files version-info.ts +coverage dist tmp src/package.json diff --git a/README.md b/README.md index 9ad86fe..fe593d4 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ yarn add package-version-info --dev ### 2. Generate the file ```bash -npx package-version-info generate --output src/version-info.ts +npx package-version-info --output src/version-info.ts ``` ```text @@ -91,18 +91,12 @@ The `author` property is omitted when `package.json` has no object-style author ## ๐Ÿงญ CLI reference -Running the CLI without arguments displays help: +Running the CLI without arguments generates `version-info.ts` from `package.json`: ```bash npx package-version-info ``` -### Commands - -| Command | Description | -| --- | --- | -| `generate` | Generate the TypeScript version information file. | - ### Options | Option | Alias | Default | Description | @@ -118,30 +112,24 @@ npx package-version-info ```bash # Generate with default paths -npx package-version-info generate +npx package-version-info # Show detailed progress -npx package-version-info generate --verbose +npx package-version-info --verbose # Use custom paths -npx package-version-info generate \ +npx package-version-info \ --input package.json \ --output src/generated/version-info.ts \ --git .git # Short aliases -npx package-version-info generate \ +npx package-version-info \ -i package.json \ -o src/generated/version-info.ts \ -g .git ``` -Flag-only generation remains supported for existing scripts: - -```bash -npx package-version-info --output src/version-info.ts -``` - ## ๐ŸŽจ Compact and verbose logging Compact mode is designed for normal builds: @@ -153,7 +141,7 @@ Compact mode is designed for normal builds: Use verbose mode when diagnosing package, author, timestamp, Git, or output-path behavior: ```bash -npx package-version-info generate --verbose +npx package-version-info --verbose ``` ## ๐Ÿ› ๏ธ Build-tool integration @@ -163,7 +151,7 @@ Generate version information automatically before your application build: ```json { "scripts": { - "version-info": "package-version-info generate --output src/version-info.ts", + "version-info": "package-version-info --output src/version-info.ts", "prebuild": "npm run version-info", "build": "tsc" } @@ -186,7 +174,7 @@ Requirements: zig build # Run the CLI -zig build run -- generate +zig build run # Run all formatting, build, and test checks yarn validate diff --git a/build.zig b/build.zig index ae0f5f3..84a3ca0 100644 --- a/build.zig +++ b/build.zig @@ -43,4 +43,25 @@ pub fn build(b: *std.Build) void { const test_step = b.step("test", "Run tests"); test_step.dependOn(&b.addRunArtifact(mod_tests).step); test_step.dependOn(&b.addRunArtifact(cli_tests).step); + + const mod_coverage_tests = b.addTest(.{ + .name = "root-coverage", + .root_module = mod, + .use_llvm = true, + }); + const cli_coverage_tests = b.addTest(.{ + .name = "cli-coverage", + .root_module = cli_mod, + .use_llvm = true, + }); + const install_mod_coverage_tests = b.addInstallArtifact(mod_coverage_tests, .{ + .dest_dir = .{ .override = .{ .custom = "coverage-tests" } }, + }); + const install_cli_coverage_tests = b.addInstallArtifact(cli_coverage_tests, .{ + .dest_dir = .{ .override = .{ .custom = "coverage-tests" } }, + }); + + const coverage_step = b.step("coverage", "Build test executables for code coverage"); + coverage_step.dependOn(&install_mod_coverage_tests.step); + coverage_step.dependOn(&install_cli_coverage_tests.step); } diff --git a/docs/assets/terminal-demo.gif b/docs/assets/terminal-demo.gif index aa27026..3852527 100644 Binary files a/docs/assets/terminal-demo.gif and b/docs/assets/terminal-demo.gif differ diff --git a/docs/terminal-demo.tape b/docs/terminal-demo.tape index 0d80e4b..d38154e 100644 --- a/docs/terminal-demo.tape +++ b/docs/terminal-demo.tape @@ -34,7 +34,7 @@ Sleep 3s Ctrl+L Sleep 500ms -Type "package-version-info generate --output generated/version-info.ts" +Type "package-version-info --output generated/version-info.ts" Enter Sleep 2s diff --git a/package.json b/package.json index 12195f4..6b14564 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,9 @@ "validate": "yarn format:check && yarn build && yarn test", "format:check": "zig fmt --check src/root.zig src/main.zig src/cli.zig build.zig", "build": "zig build", - "start": "zig build run -- generate", + "start": "zig build run", "test": "zig build test", + "coverage:build": "rimraf coverage zig-out/coverage-tests && zig build coverage", "clean": "rimraf zig-out dist", "prepare": "husky", "copy-files": "copyfiles -u 2 zig-out/bin/version_info dist/bin && copyfiles README.md LICENSE dist", diff --git a/scripts/coverage.sh b/scripts/coverage.sh new file mode 100644 index 0000000..d7de1b9 --- /dev/null +++ b/scripts/coverage.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +set -eu + +workspace=$(pwd) +coverage_dir="$workspace/coverage" +tests_dir="$workspace/zig-out/coverage-tests" + +rm -rf "$coverage_dir" +mkdir -p "$coverage_dir/root" "$coverage_dir/cli" "$coverage_dir/merged" + +kcov \ + --include-path="$workspace/src" \ + "$coverage_dir/root" \ + "$tests_dir/root-coverage" + +kcov \ + --include-path="$workspace/src" \ + "$coverage_dir/cli" \ + "$tests_dir/cli-coverage" + +kcov \ + --merge \ + "$coverage_dir/merged" \ + "$coverage_dir/root" \ + "$coverage_dir/cli" + +sed -i \ + 's/ branch-rate="[^"]*" branches-covered="[^"]*" branches-rate="[^"]*"/ branch-rate="0" branches-covered="0" branches-valid="0"/' \ + "$coverage_dir/merged/kcov-merged/cobertura.xml" diff --git a/src/cli.zig b/src/cli.zig index 288f263..b1a3b76 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -29,50 +29,39 @@ pub const Action = union(enum) { pub fn parse(args: anytype) Action { var options: GenerateOptions = .{}; - var generate = false; while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { return .help; } else if (std.mem.eql(u8, arg, "--version") or std.mem.eql(u8, arg, "-v")) { return .version; - } else if (std.mem.eql(u8, arg, "generate")) { - generate = true; } else if (std.mem.eql(u8, arg, "--verbose")) { options.verbose = true; - generate = true; } else if (std.mem.eql(u8, arg, "--input") or std.mem.eql(u8, arg, "-i")) { const value = args.next() orelse return .{ .invalid = .{ .missing_value = arg } }; if (std.mem.startsWith(u8, value, "-")) return .{ .invalid = .{ .missing_value = arg } }; options.input_path = value; - generate = true; } else if (std.mem.eql(u8, arg, "--output") or std.mem.eql(u8, arg, "-o")) { const value = args.next() orelse return .{ .invalid = .{ .missing_value = arg } }; if (std.mem.startsWith(u8, value, "-")) return .{ .invalid = .{ .missing_value = arg } }; options.output_path = value; - generate = true; } else if (std.mem.eql(u8, arg, "--git") or std.mem.eql(u8, arg, "-g")) { const value = args.next() orelse return .{ .invalid = .{ .missing_value = arg } }; if (std.mem.startsWith(u8, value, "-")) return .{ .invalid = .{ .missing_value = arg } }; options.git_path = value; - generate = true; } else { return .{ .invalid = .{ .unknown = arg } }; } } - return if (generate) .{ .generate = options } else .help; + return .{ .generate = options }; } pub fn writeHelp(writer: anytype) !void { try writer.writeAll( color.bright_cyan ++ "Package Version Info" ++ color.reset ++ "\n\n" ++ color.yellow ++ "Usage:" ++ color.reset ++ - "\n package-version-info generate [options]" ++ - "\n package-version-info [generation options]" ++ - "\n\n" ++ color.yellow ++ "Commands:" ++ color.reset ++ - "\n " ++ color.green ++ "generate" ++ color.reset ++ - " Generate TypeScript version information." ++ + "\n package-version-info [options]" ++ "\n\n" ++ color.yellow ++ "Options:" ++ color.reset ++ "\n " ++ color.green ++ "-h, --help" ++ color.reset ++ " Display usage information." ++ @@ -119,11 +108,17 @@ fn parseTest(args: []const []const u8) Action { return parse(&iterator); } -test "no arguments and help flags show help" { - try std.testing.expectEqual(.help, std.meta.activeTag(parseTest(&.{}))); +test "no arguments generate with default paths" { + const options = parseTest(&.{}).generate; + try std.testing.expectEqualStrings("package.json", options.input_path); + try std.testing.expectEqualStrings("version-info.ts", options.output_path); + try std.testing.expectEqualStrings(".git", options.git_path); + try std.testing.expect(!options.verbose); +} + +test "help flags show help" { try std.testing.expectEqual(.help, std.meta.activeTag(parseTest(&.{"-h"}))); try std.testing.expectEqual(.help, std.meta.activeTag(parseTest(&.{"--help"}))); - try std.testing.expectEqual(.help, std.meta.activeTag(parseTest(&.{ "generate", "--help" }))); } test "version flags show version" { @@ -131,16 +126,7 @@ test "version flags show version" { try std.testing.expectEqual(.version, std.meta.activeTag(parseTest(&.{"--version"}))); } -test "generate command uses default paths" { - const action = parseTest(&.{"generate"}); - const options = action.generate; - try std.testing.expectEqualStrings("package.json", options.input_path); - try std.testing.expectEqualStrings("version-info.ts", options.output_path); - try std.testing.expectEqualStrings(".git", options.git_path); - try std.testing.expect(!options.verbose); -} - -test "legacy generation options remain supported" { +test "generation options are parsed" { const action = parseTest(&.{ "--verbose", "--input", @@ -157,18 +143,18 @@ test "legacy generation options remain supported" { try std.testing.expect(options.verbose); } -test "verbose triggers generation with or without command" { - const legacy = parseTest(&.{"--verbose"}).generate; - try std.testing.expect(legacy.verbose); - try std.testing.expectEqualStrings("package.json", legacy.input_path); - - const command = parseTest(&.{ "generate", "--verbose" }).generate; - try std.testing.expect(command.verbose); +test "verbose enables detailed generation progress" { + const options = parseTest(&.{"--verbose"}).generate; + try std.testing.expect(options.verbose); + try std.testing.expectEqualStrings("package.json", options.input_path); } test "unknown argument is invalid" { const action = parseTest(&.{"--unknown"}); try std.testing.expectEqualStrings("--unknown", action.invalid.unknown); + + const removed_command = parseTest(&.{"generate"}); + try std.testing.expectEqualStrings("generate", removed_command.invalid.unknown); } test "options require values" { @@ -188,8 +174,9 @@ test "help contains usage commands and options" { try writeHelp(&output.writer); const help = output.writer.buffered(); try std.testing.expect(std.mem.indexOf(u8, help, "Usage:") != null); - try std.testing.expect(std.mem.indexOf(u8, help, "Commands:") != null); try std.testing.expect(std.mem.indexOf(u8, help, "Options:") != null); + try std.testing.expect(std.mem.indexOf(u8, help, "Commands:") == null); + try std.testing.expect(std.mem.indexOf(u8, help, " generate") == null); try std.testing.expect(std.mem.indexOf(u8, help, "--help") != null); try std.testing.expect(std.mem.indexOf(u8, help, "--verbose") != null); }