Tool to recursively find Cargo.toml files and update the rust-version value to the latest installed Rust.

Add Claude Code generated tests and CLAUDE.md.

Signed-off-by: moderation <michael@sooper.org>

+205
+61
CLAUDE.md
··· 1 + # CLAUDE.md 2 + 3 + ## Project Overview 4 + 5 + This is `rust-version-updater`, a CLI tool that recursively updates `rust-version` fields in Cargo.toml files to match the currently installed Rust version. 6 + 7 + ## Build Requirements 8 + 9 + This project targets **Zig master** (tip of tree). It uses unstable standard library APIs that are not available in stable releases. 10 + 11 + Current development version: `0.16.0-dev.2368+380ea6fb5` 12 + 13 + ## Commands 14 + 15 + ```sh 16 + # Build 17 + zig build 18 + 19 + # Build release 20 + zig build -Doptimize=ReleaseFast 21 + 22 + # Run 23 + zig build run 24 + 25 + # Run with arguments 26 + zig build run -- --dry-run 27 + 28 + # Test 29 + zig build test 30 + ``` 31 + 32 + ## Project Structure 33 + 34 + ``` 35 + . 36 + ├── build.zig # Build configuration 37 + ├── build.zig.zon # Package manifest 38 + └── src/ 39 + └── main.zig # All application code and tests 40 + ``` 41 + 42 + ## Code Patterns 43 + 44 + - Uses the new `std.Io` and `std.process` APIs from Zig master 45 + - Entry point is `pub fn main(init: process.Init)` (not the traditional `pub fn main() void`) 46 + - Buffered I/O with explicit flush calls 47 + - Tests are inline at the bottom of main.zig using `test` blocks 48 + - Memory allocation uses the provided `init.gpa` allocator 49 + - All allocations are paired with corresponding `defer` frees 50 + 51 + ## Key Types 52 + 53 + - `Version` - Semver parsing and comparison 54 + - `ProcessResult` / `ProcessStatus` - Result of processing a Cargo.toml file 55 + - `Stats` - Tracks scanned/updated/skipped/error counts 56 + 57 + ## Testing 58 + 59 + Tests cover the pure functions (`Version.parse`, `Version.isOlderThan`, `processContent`). The I/O-dependent functions are not unit tested. 60 + 61 + Run tests with: `zig build test`
+12
build.zig
··· 26 26 27 27 const run_step = b.step("run", "Run the rust-version-updater"); 28 28 run_step.dependOn(&run_cmd.step); 29 + 30 + const unit_tests = b.addTest(.{ 31 + .root_module = b.createModule(.{ 32 + .root_source_file = b.path("src/main.zig"), 33 + .target = target, 34 + .optimize = optimize, 35 + }), 36 + }); 37 + 38 + const run_unit_tests = b.addRunArtifact(unit_tests); 39 + const test_step = b.step("test", "Run unit tests"); 40 + test_step.dependOn(&run_unit_tests.step); 29 41 }
+132
src/main.zig
··· 302 302 303 303 return ProcessResult{ .status = .not_found, .old_version = null, .new_content = null }; 304 304 } 305 + 306 + // Tests 307 + 308 + test "Version.parse valid versions" { 309 + const v1 = Version.parse("1.70.0").?; 310 + try std.testing.expectEqual(@as(u32, 1), v1.major); 311 + try std.testing.expectEqual(@as(u32, 70), v1.minor); 312 + try std.testing.expectEqual(@as(u32, 0), v1.patch); 313 + 314 + const v2 = Version.parse("1.84.1").?; 315 + try std.testing.expectEqual(@as(u32, 1), v2.major); 316 + try std.testing.expectEqual(@as(u32, 84), v2.minor); 317 + try std.testing.expectEqual(@as(u32, 1), v2.patch); 318 + 319 + // Two-part version (patch defaults to 0) 320 + const v3 = Version.parse("1.70").?; 321 + try std.testing.expectEqual(@as(u32, 1), v3.major); 322 + try std.testing.expectEqual(@as(u32, 70), v3.minor); 323 + try std.testing.expectEqual(@as(u32, 0), v3.patch); 324 + } 325 + 326 + test "Version.parse with whitespace and quotes" { 327 + const v1 = Version.parse(" 1.70.0 ").?; 328 + try std.testing.expectEqual(@as(u32, 1), v1.major); 329 + try std.testing.expectEqual(@as(u32, 70), v1.minor); 330 + 331 + const v2 = Version.parse("\"1.84.0\"").?; 332 + try std.testing.expectEqual(@as(u32, 1), v2.major); 333 + try std.testing.expectEqual(@as(u32, 84), v2.minor); 334 + } 335 + 336 + test "Version.parse invalid versions" { 337 + try std.testing.expectEqual(@as(?Version, null), Version.parse("")); 338 + try std.testing.expectEqual(@as(?Version, null), Version.parse("abc")); 339 + try std.testing.expectEqual(@as(?Version, null), Version.parse("1")); 340 + try std.testing.expectEqual(@as(?Version, null), Version.parse("1.x.0")); 341 + } 342 + 343 + test "Version.isOlderThan" { 344 + const v1_70 = Version{ .major = 1, .minor = 70, .patch = 0 }; 345 + const v1_84 = Version{ .major = 1, .minor = 84, .patch = 0 }; 346 + const v1_84_1 = Version{ .major = 1, .minor = 84, .patch = 1 }; 347 + const v2_0 = Version{ .major = 2, .minor = 0, .patch = 0 }; 348 + 349 + try std.testing.expect(v1_70.isOlderThan(v1_84)); 350 + try std.testing.expect(!v1_84.isOlderThan(v1_70)); 351 + try std.testing.expect(!v1_84.isOlderThan(v1_84)); // equal 352 + try std.testing.expect(v1_84.isOlderThan(v1_84_1)); 353 + try std.testing.expect(v1_84_1.isOlderThan(v2_0)); 354 + } 355 + 356 + test "processContent updates older version" { 357 + const content = "[package]\nrust-version = \"1.70.0\"\n"; 358 + const current = Version{ .major = 1, .minor = 84, .patch = 0 }; 359 + 360 + const result = try processContent(std.testing.allocator, content, current); 361 + defer if (result.new_content) |nc| std.testing.allocator.free(nc); 362 + 363 + try std.testing.expectEqual(ProcessStatus.updated, result.status); 364 + try std.testing.expectEqualStrings("1.70.0", result.old_version.?); 365 + try std.testing.expectEqualStrings("[package]\nrust-version = \"1.84.0\"\n", result.new_content.?); 366 + } 367 + 368 + test "processContent skips newer version" { 369 + const content = "[package]\nrust-version = \"1.84.0\"\n"; 370 + const current = Version{ .major = 1, .minor = 70, .patch = 0 }; 371 + 372 + const result = try processContent(std.testing.allocator, content, current); 373 + 374 + try std.testing.expectEqual(ProcessStatus.skipped_not_older, result.status); 375 + try std.testing.expectEqual(@as(?[]u8, null), result.new_content); 376 + } 377 + 378 + test "processContent skips equal version" { 379 + const content = "[package]\nrust-version = \"1.84.0\"\n"; 380 + const current = Version{ .major = 1, .minor = 84, .patch = 0 }; 381 + 382 + const result = try processContent(std.testing.allocator, content, current); 383 + 384 + try std.testing.expectEqual(ProcessStatus.skipped_not_older, result.status); 385 + } 386 + 387 + test "processContent skips workspace inheritance" { 388 + const content = "[package]\nrust-version = { workspace = true }\n"; 389 + const current = Version{ .major = 1, .minor = 84, .patch = 0 }; 390 + 391 + const result = try processContent(std.testing.allocator, content, current); 392 + 393 + try std.testing.expectEqual(ProcessStatus.skipped_non_semver, result.status); 394 + } 395 + 396 + test "processContent not found" { 397 + const content = "[package]\nname = \"foo\"\nversion = \"0.1.0\"\n"; 398 + const current = Version{ .major = 1, .minor = 84, .patch = 0 }; 399 + 400 + const result = try processContent(std.testing.allocator, content, current); 401 + 402 + try std.testing.expectEqual(ProcessStatus.not_found, result.status); 403 + } 404 + 405 + test "processContent preserves surrounding content" { 406 + const content = 407 + \\[package] 408 + \\name = "myapp" 409 + \\version = "0.1.0" 410 + \\rust-version = "1.70.0" 411 + \\edition = "2021" 412 + \\ 413 + \\[dependencies] 414 + \\serde = "1.0" 415 + \\ 416 + ; 417 + const current = Version{ .major = 1, .minor = 84, .patch = 0 }; 418 + 419 + const result = try processContent(std.testing.allocator, content, current); 420 + defer if (result.new_content) |nc| std.testing.allocator.free(nc); 421 + 422 + try std.testing.expectEqual(ProcessStatus.updated, result.status); 423 + 424 + const expected = 425 + \\[package] 426 + \\name = "myapp" 427 + \\version = "0.1.0" 428 + \\rust-version = "1.84.0" 429 + \\edition = "2021" 430 + \\ 431 + \\[dependencies] 432 + \\serde = "1.0" 433 + \\ 434 + ; 435 + try std.testing.expectEqualStrings(expected, result.new_content.?); 436 + }