From 29bb50d366015fede4ebfa7a45b1fd270ac79309 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 20 Jul 2025 19:10:02 +0100 Subject: [PATCH 1/5] test: add reproducer of derived project error --- test/test_testitems.jl | 57 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/test_testitems.jl b/test/test_testitems.jl index a1ac98f..5a96b29 100644 --- a/test/test_testitems.jl +++ b/test/test_testitems.jl @@ -439,3 +439,60 @@ end @test ti.name == "Test1" end + +@testitem "fallback project without manifest causes content_hash error" begin + using JuliaWorkspaces + using JuliaWorkspaces.URIs2: filepath2uri + + mktempdir() do temp_dir + # Create fallback project with Project.toml but NO Manifest.toml + fallback_dir = joinpath(temp_dir, "FallbackProject") + mkpath(fallback_dir) + + fallback_project_toml = joinpath(fallback_dir, "Project.toml") + write( + fallback_project_toml, + """ +name = "FallbackProject" +uuid = "12345678-1234-1234-1234-123456789abc" +version = "0.1.0" +""", + ) + # NO MANIFEST - this makes derived_project return nothing + + # Create a Julia file in a completely separate directory (not under any project) + separate_dir = joinpath(temp_dir, "SeparateDir") + mkpath(separate_dir) + julia_file = joinpath(separate_dir, "isolated.jl") + write(julia_file, "# Julia file not under any project") + + # Add files to workspace + fallback_project_uri = filepath2uri(fallback_project_toml) + julia_uri = filepath2uri(julia_file) + fallback_folder_uri = filepath2uri(fallback_dir) + + jw = JuliaWorkspace() + add_file!( + jw, + TextFile( + fallback_project_uri, + SourceText(read(fallback_project_toml, String), "toml"), + ), + ) + add_file!(jw, TextFile(julia_uri, SourceText("# test", "julia"))) + + # Set the no-manifest project as fallback + JuliaWorkspaces.set_input_fallback_test_project!(jw.runtime, fallback_folder_uri) + + # Manually trigger the exact bug condition that was demonstrated in final_bug_reproducer.jl + rt = jw.runtime + fallback = JuliaWorkspaces.input_fallback_test_project(rt) + + # Verify bug conditions: fallback exists but derived_project returns nothing + fallback_derived = JuliaWorkspaces.derived_project(rt, fallback) + @test fallback_derived === nothing + + # This line mimics what happens on line 132 of derived_testenv when the bug occurs + @test_throws "type Nothing has no field content_hash" fallback_derived.content_hash + end +end From c39c99dd1a67fbbc70353155bd0f80588fa789d5 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 20 Jul 2025 19:29:24 +0100 Subject: [PATCH 2/5] fix missing Manifest-v1.11.toml --- src/fileio.jl | 3 +- test/test_testitems.jl | 99 +++++++++++++++++++++++------------------- 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/src/fileio.jl b/src/fileio.jl index 2b9e950..4a514e5 100644 --- a/src/fileio.jl +++ b/src/fileio.jl @@ -11,7 +11,8 @@ end function is_path_manifest_file(path) basename_lower_case = basename(lowercase(path)) - return basename_lower_case=="manifest.toml" || basename_lower_case=="juliamanifest.toml" + # Manifest.toml, Manifest-v1.11.toml, JuliaManifest.toml, etc. + return occursin(r"^manifest(\-v\d+(\.\d+)*)?\.toml$", basename_lower_case) || basename_lower_case == "juliamanifest.toml" end function is_path_lintconfig_file(path) diff --git a/test/test_testitems.jl b/test/test_testitems.jl index 5a96b29..4aedacc 100644 --- a/test/test_testitems.jl +++ b/test/test_testitems.jl @@ -440,59 +440,68 @@ end @test ti.name == "Test1" end -@testitem "fallback project without manifest causes content_hash error" begin +@testitem "versioned manifest files are detected" begin using JuliaWorkspaces using JuliaWorkspaces.URIs2: filepath2uri mktempdir() do temp_dir - # Create fallback project with Project.toml but NO Manifest.toml - fallback_dir = joinpath(temp_dir, "FallbackProject") - mkpath(fallback_dir) - - fallback_project_toml = joinpath(fallback_dir, "Project.toml") - write( - fallback_project_toml, - """ -name = "FallbackProject" + # Create project with versioned manifest + project_dir = joinpath(temp_dir, "VersionedProject") + mkpath(project_dir) + + project_file = joinpath(project_dir, "Project.toml") + write(project_file, """ +name = "VersionedProject" uuid = "12345678-1234-1234-1234-123456789abc" version = "0.1.0" -""", - ) - # NO MANIFEST - this makes derived_project return nothing - - # Create a Julia file in a completely separate directory (not under any project) - separate_dir = joinpath(temp_dir, "SeparateDir") - mkpath(separate_dir) - julia_file = joinpath(separate_dir, "isolated.jl") - write(julia_file, "# Julia file not under any project") - - # Add files to workspace - fallback_project_uri = filepath2uri(fallback_project_toml) - julia_uri = filepath2uri(julia_file) - fallback_folder_uri = filepath2uri(fallback_dir) +[deps] +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +""") + + # Create versioned manifest + versioned_manifest = joinpath(project_dir, "Manifest-v$(VERSION.major).$(VERSION.minor).toml") + write(versioned_manifest, """ +julia_version = "$(VERSION.major).$(VERSION.minor).$(VERSION.patch)" +manifest_format = "2.0" +project_hash = "test" + +[[deps.Random]] +deps = ["SHA", "Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +version = "1.10.0" + +[[deps.SHA]] +uuid = "ea8e919c-285b-4e28-92e2-21d1dda8b7a7" +version = "0.7.0" + +[[deps.Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +version = "1.10.0" +""") + + # Add to workspace + project_uri = filepath2uri(project_file) + manifest_uri = filepath2uri(versioned_manifest) + folder_uri = filepath2uri(project_dir) + jw = JuliaWorkspace() - add_file!( - jw, - TextFile( - fallback_project_uri, - SourceText(read(fallback_project_toml, String), "toml"), - ), - ) - add_file!(jw, TextFile(julia_uri, SourceText("# test", "julia"))) - - # Set the no-manifest project as fallback - JuliaWorkspaces.set_input_fallback_test_project!(jw.runtime, fallback_folder_uri) - - # Manually trigger the exact bug condition that was demonstrated in final_bug_reproducer.jl + add_file!(jw, TextFile(project_uri, SourceText(read(project_file, String), "toml"))) + add_file!(jw, TextFile(manifest_uri, SourceText(read(versioned_manifest, String), "toml"))) + + # Test that versioned manifest IS now detected rt = jw.runtime - fallback = JuliaWorkspaces.input_fallback_test_project(rt) - - # Verify bug conditions: fallback exists but derived_project returns nothing - fallback_derived = JuliaWorkspaces.derived_project(rt, fallback) - @test fallback_derived === nothing - - # This line mimics what happens on line 132 of derived_testenv when the bug occurs - @test_throws "type Nothing has no field content_hash" fallback_derived.content_hash + potential_projects = JuliaWorkspaces.derived_potential_project_folders(rt) + + @test haskey(potential_projects, folder_uri) + project_info = potential_projects[folder_uri] + @test project_info.project_file !== nothing + @test project_info.manifest_file !== nothing # FIXED: versioned manifest now detected + + # This should now return a valid project + derived_result = JuliaWorkspaces.derived_project(rt, folder_uri) + @test derived_result !== nothing + @test derived_result isa JuliaWorkspaces.JuliaProject end end + From 26d8b1b5b57b3aad09f63c693fecb3507a701628 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 20 Jul 2025 20:04:23 +0100 Subject: [PATCH 3/5] make derived_testenv safer and cleaner --- src/JuliaWorkspaces.jl | 2 ++ src/layer_testitems.jl | 58 +++++++++++++++++++++++++++--------------- src/utils.jl | 7 +++++ test/test_testitems.jl | 49 +++++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 src/utils.jl diff --git a/src/JuliaWorkspaces.jl b/src/JuliaWorkspaces.jl index b3fce9e..d1b4f1b 100644 --- a/src/JuliaWorkspaces.jl +++ b/src/JuliaWorkspaces.jl @@ -7,6 +7,8 @@ using Salsa using AutoHashEquals +include("utils.jl") + include("compat.jl") import Pkg diff --git a/src/layer_testitems.jl b/src/layer_testitems.jl index 33585cc..9b67579 100644 --- a/src/layer_testitems.jl +++ b/src/layer_testitems.jl @@ -109,32 +109,50 @@ Salsa.@derived function derived_testenv(rt, uri) projects = derived_project_folders(rt) packages = derived_package_folders(rt) - project_uri = find_project_for_file(projects, uri) + project_uri_guess = @something( + find_project_for_file(projects, uri), + input_fallback_test_project(rt), + Some(nothing) + ) package_uri = find_package_for_file(packages, uri) - if project_uri === nothing - project_uri = input_fallback_test_project(rt) - end - - package_name = package_uri === nothing ? nothing : derived_package(rt, package_uri).name + package_name = + if isnothing(package_uri) + nothing + else + safe_getproperty(derived_package(rt, package_uri), :name) + end - if project_uri == package_uri - elseif project_uri in projects - relevant_project = derived_project(rt, project_uri) + project_uri = + if project_uri_guess == package_uri + project_uri_guess + elseif project_uri_guess in projects + relevant_project = derived_project(rt, project_uri_guess) + + if isnothing(relevant_project) + nothing + elseif any(i->i.uri == package_uri, collect(values(relevant_project.deved_packages))) + project_uri_guess + else + nothing + end + else + nothing + end - if findfirst(i->i.uri == package_uri, collect(values(relevant_project.deved_packages))) === nothing - project_uri = nothing + project_env_content_hash = + if isnothing(project_uri) + hash(nothing) + else + safe_getproperty(derived_project(rt, project_uri), :content_hash) end - else - project_uri = nothing - end - env_content_hash = isnothing(project_uri) ? hash(nothing) : derived_project(rt, project_uri).content_hash - if package_uri===nothing - env_content_hash = hash(nothing, env_content_hash) - else - env_content_hash = hash(derived_package(rt, package_uri).content_hash) - end + env_content_hash = + if isnothing(package_uri) + hash(project_env_content_hash) + else + safe_getproperty(derived_package(rt, package_uri), :content_hash) + end # We construct a string for the env content hash here so that later when we # deserialize it with JSON.jl we don't end up with Int conversion issues diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 0000000..b066725 --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,7 @@ +@inline function safe_getproperty(x, s::Symbol) + if isnothing(x) + return nothing + else + return getproperty(x, s) + end +end diff --git a/test/test_testitems.jl b/test/test_testitems.jl index 4aedacc..21aaed5 100644 --- a/test/test_testitems.jl +++ b/test/test_testitems.jl @@ -505,3 +505,52 @@ version = "1.10.0" end end +@testitem "handle missing manifest gracefully" begin + using JuliaWorkspaces + using JuliaWorkspaces.URIs2: filepath2uri + + mktempdir() do temp_dir + # Create a simple project that will work + project_dir = joinpath(temp_dir, "SimpleProject") + mkpath(project_dir) + + project_file = joinpath(project_dir, "Project.toml") + write( + project_file, + """ +name = "SimpleProject" +uuid = "12345678-1234-1234-1234-123456789abc" +version = "0.1.0" +""", + ) + + # NO MANIFEST - this makes derived_project return nothing + + # Create Julia file + test_file = joinpath(temp_dir, "test.jl") + write(test_file, "# Test file") + + # Add to workspace + project_uri = filepath2uri(project_file) + test_uri = filepath2uri(test_file) + folder_uri = filepath2uri(project_dir) + + jw = JuliaWorkspace() + add_file!(jw, TextFile(project_uri, SourceText(read(project_file, String), "toml"))) + add_file!(jw, TextFile(test_uri, SourceText("# test", "julia"))) + + # Set project as fallback test project + JuliaWorkspaces.set_input_fallback_test_project!(jw.runtime, folder_uri) + + rt = jw.runtime + + # Verify derived_project returns nothing (no manifest) + derived_result = JuliaWorkspaces.derived_project(rt, folder_uri) + @test derived_result === nothing + + # This should work with defensive programming - no crash on .content_hash + test_env = get_test_env(jw, test_uri) + @test test_env isa JuliaWorkspaces.JuliaTestEnv + @test test_env.project_uri === nothing # Should be set to nothing due to lack of manifest + end +end From 909db9f00a62684a8d444d09c2b160f22ff8d937 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 20 Jul 2025 20:09:55 +0100 Subject: [PATCH 4/5] single regexp --- src/fileio.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fileio.jl b/src/fileio.jl index 4a514e5..b7dd4db 100644 --- a/src/fileio.jl +++ b/src/fileio.jl @@ -12,7 +12,7 @@ function is_path_manifest_file(path) basename_lower_case = basename(lowercase(path)) # Manifest.toml, Manifest-v1.11.toml, JuliaManifest.toml, etc. - return occursin(r"^manifest(\-v\d+(\.\d+)*)?\.toml$", basename_lower_case) || basename_lower_case == "juliamanifest.toml" + return occursin(r"^(julia)?manifest(\-v\d+(\.\d+)*)?\.toml$", basename_lower_case) end function is_path_lintconfig_file(path) From d5463c6e120a7cd6f2ddae4baf8763a811931729 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 20 Jul 2025 20:13:31 +0100 Subject: [PATCH 5/5] ensure final output is a UInt --- src/layer_testitems.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layer_testitems.jl b/src/layer_testitems.jl index 9b67579..4a9e6dc 100644 --- a/src/layer_testitems.jl +++ b/src/layer_testitems.jl @@ -151,7 +151,7 @@ Salsa.@derived function derived_testenv(rt, uri) if isnothing(package_uri) hash(project_env_content_hash) else - safe_getproperty(derived_package(rt, package_uri), :content_hash) + hash(safe_getproperty(derived_package(rt, package_uri), :content_hash)) end # We construct a string for the env content hash here so that later when we