diff --git a/Project.toml b/Project.toml index 6f715ae56..50f111b7d 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,7 @@ authors = ["Shuhei Kadowaki "] projects = ["test"] [deps] +Configurations = "5218b696-f38b-4ac9-8b61-a12ec717816d" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" JSONRPC = "a2756949-8476-49a1-a294-231eace0f283" JuliaLowering = "f3c80556-a63f-4383-b822-37d64f81a311" @@ -27,6 +28,7 @@ JuliaSyntax = {rev = "jetls-hacking-2", url = "https://github.com/JuliaLang/Juli LSP = {path = "LSP"} [compat] +Configurations = "0.17.6" JET = "0.10.6" JSONRPC = "0.1" JuliaLowering = "1" diff --git a/src/analysis/full-analysis.jl b/src/analysis/full-analysis.jl index 84a119fe0..ae84b4f58 100644 --- a/src/analysis/full-analysis.jl +++ b/src/analysis/full-analysis.jl @@ -109,7 +109,7 @@ function request_analysis!( end should_queue || @goto wait_or_return # Request saved as pending - debounce = get_config(server.state.config_manager, "full_analysis", "debounce") + debounce = get_config(server.state.config_manager, :full_analysis, :debounce) if onsave && debounce isa Float64 && debounce > 0 local delay::Float64 = debounce store!(manager.debounced) do debounced diff --git a/src/config.jl b/src/config.jl index 13a10ebd6..a1ed28cef 100644 --- a/src/config.jl +++ b/src/config.jl @@ -1,92 +1,3 @@ -# TODO (later): move this definition to external files -const DEFAULT_CONFIG = ConfigDict( - "full_analysis" => ConfigDict( - "debounce" => 1.0 - ), - "testrunner" => ConfigDict( - "executable" => @static Sys.iswindows() ? "testrunner.bat" : "testrunner" - ), - "formatter" => ConfigDict( - "runic" => ConfigDict( - "executable" => @static Sys.iswindows() ? "runic.bat" : "runic" - ) - ), - "internal" => ConfigDict( - "static_setting" => 0 - ), -) - -const STATIC_CONFIG = ConfigDict( - "full_analysis" => ConfigDict( - "debounce" => false - ), - "testrunner" => ConfigDict( - "executable" => false - ), - "formatter" => ConfigDict( - "runic" => ConfigDict( - "executable" => false - ) - ), - "internal" => ConfigDict( - "static_setting" => true - ), -) - -function access_nested_dict(dict::ConfigDict, path::String, rest_path::String...) - nextobj = @something get(dict, path, nothing) return nothing - if !(nextobj isa ConfigDict) - if isempty(rest_path) - return nextobj - else - return nothing - end - end - return access_nested_dict(nextobj, rest_path...) -end - -""" - traverse_merge(on_leaf, base::ConfigDict, overlay::ConfigDict) -> merged::ConfigDict - -Return a new `ConfigDict` whose key value pairs are merged from `base` and `overlay`. - -If a key in `overlay` is a dictionary, it will recursively merge it into the corresponding -key in `base`, creating new `ConfigDict` instances along the way. - -When a value in `overlay` is not a dictionary, the `on_leaf` function is called with: -- `current_path`: the current path as a vector of strings -- `v`: the value from `overlay` -The `on_leaf(current_path, v) -> newv` function should return the value to be stored -in the result, or `nothing` to skip storing the key. -""" -function traverse_merge( - on_leaf, base::ConfigDict, overlay::ConfigDict, - key_path::Vector{String} = String[] - ) - result = base - for (k, v) in overlay - current_path = [key_path; k] - if v isa ConfigDict - base_v = get(base, k, nothing) - if base_v isa ConfigDict - merged_v = traverse_merge(on_leaf, base_v, v, current_path) - result = ConfigDict(result, k => merged_v) - else - merged_v = traverse_merge(on_leaf, ConfigDict(), v, current_path) - result = ConfigDict(result, k => merged_v) - end - else - on_leaf_result = on_leaf(current_path, v) - if on_leaf_result !== nothing - result = ConfigDict(result, k => on_leaf_result) - end - end - end - return result -end - -is_static_setting(key_path::String...) = access_nested_dict(STATIC_CONFIG, key_path...) === true - is_config_file(filepath::AbstractString) = filepath == "__DEFAULT_CONFIG__" || basename(filepath) == ".JETLSConfig.toml" """ @@ -97,7 +8,7 @@ The order is determined by the following rule: 1. "__DEFAULT_CONFIG__" has lower priority than any other path. -This rules defines a total order. (See `is_config_file`) +This rule defines a total order. (See `is_config_file`) """ function Base.lt(::ConfigFileOrder, path1, path2) path1 == "__DEFAULT_CONFIG__" && return false @@ -106,49 +17,96 @@ function Base.lt(::ConfigFileOrder, path1, path2) return false # unreachable end -function cleanup_empty_dicts(dict::ConfigDict) - result = dict - for (k, v) in dict - if v isa ConfigDict - cleaned_v = cleanup_empty_dicts(v) - if isempty(cleaned_v) - result = Base.delete(result, k) - elseif cleaned_v != v - result = ConfigDict(result, k => cleaned_v) - end - end +@generated function on_difference( + callback, + old_config::T, + new_config::T, + path::NTuple{N,Symbol}=() +) where {T<:ConfigSection,N} + entries = ( + :(on_difference( + callback, + getfield(old_config, $(QuoteNode(fname))), + getfield(new_config, $(QuoteNode(fname))), + (path..., $(QuoteNode(fname))) + )) + for fname in fieldnames(T) + ) + + quote + $T($(entries...)) end - return result end -function merge_settings(base::ConfigDict, overlay::ConfigDict) - return traverse_merge(base, overlay) do _, v - v - end |> cleanup_empty_dicts -end +@generated function on_difference( + callback, + old_val::T, + new_val::Nothing, + path::Tuple +) where T <: ConfigSection + entries = ( + :(on_difference( + callback, + getfield(old_val, $(QuoteNode(fname))), + nothing, + (path..., $(QuoteNode(fname))) + )) + for fname in fieldnames(T) + ) -function get_settings(data::ConfigManagerData) - result = ConfigDict() - for config in Iterators.reverse(values(data.watched_files)) - result = merge_settings(result, config) + quote + $T($(entries...)) end - return result end -function merge_static_settings(base::ConfigDict, overlay::ConfigDict) - return traverse_merge(base, overlay) do path, v - is_static_setting(path...) ? v : nothing - end |> cleanup_empty_dicts +@generated function on_difference( + callback, + old_val::Nothing, + new_val::T, + path::Tuple +) where T <: ConfigSection + entries = ( + :(on_difference( + callback, + nothing, + getfield(new_val, $(QuoteNode(fname))), + (path..., $(QuoteNode(fname))) + )) + for fname in fieldnames(T) + ) + + quote + $T($(entries...)) + end end -function get_static_settings(data::ConfigManagerData) - result = ConfigDict() - for config in Iterators.reverse(values(data.watched_files)) - result = merge_static_settings(result, config) +on_difference(callback, old_val, new_val, path::Tuple) = + old_val !== new_val ? callback(old_val, new_val, path) : old_val + +""" + merge_setting(base::T, overlay::T) where {T<:ConfigSection} -> T + +Merges two configuration objects, with `overlay` taking precedence over `base`. +If a field in `overlay` is `nothing`, the corresponding field from `base` is retained. +""" +merge_setting(base::T, overlay::T) where {T<:ConfigSection} = + on_difference((base_val, overlay_val, path) -> overlay_val === nothing ? base_val : overlay_val, base, overlay) + +function get_current_settings(watched_files::WatchedConfigFiles) + result = DEFAULT_CONFIG + for config in Iterators.reverse(values(watched_files)) + result = merge_setting(result, config) end return result end +# TODO: remove this. +# (now this is used for `collect_unmatched_keys` only. see that's comment) +const ConfigDict = Base.PersistentDict{String, Any} +to_config_dict(dict::AbstractDict) = ConfigDict((k => (v isa AbstractDict ? to_config_dict(v) : v) for (k, v) in dict)...) + +const DEFAULT_CONFIG_DICT = to_config_dict(Configurations.to_dict(DEFAULT_CONFIG)) + """ collect_unmatched_keys(this::ConfigDict, ref::ConfigDict) -> Vector{Vector{String}} @@ -178,8 +136,12 @@ julia> collect_unmatched_keys( 1-element Vector{Vector{String}}: ["key1"] ``` + + +TODO: remove this. This is a temporary workaround to report unknown keys in the config file + until Configurations.jl supports reporting full path of unknown keys. """ -function collect_unmatched_keys(this::ConfigDict, ref::ConfigDict=DEFAULT_CONFIG) +function collect_unmatched_keys(this::ConfigDict, ref::ConfigDict=DEFAULT_CONFIG_DICT) unknown_keys = Vector{String}[] collect_unmatched_keys!(unknown_keys, this, ref, String[]) return unknown_keys @@ -211,19 +173,20 @@ Retrieves the current configuration value. Among the registered configuration files, fetches the value in order of priority (see `Base.lt(::ConfigFileOrder, path1, path2)`). If the key path does not exist in any of the configurations, returns `nothing`. """ -function get_config(manager::ConfigManager, key_path::String...) - is_static_setting(key_path...) && - return access_nested_dict(load(manager).static_settings, key_path...) - for config in values(load(manager).watched_files) - return @something access_nested_dict(config, key_path...) continue +Base.@constprop :aggressive function get_config(manager::ConfigManager, key_path::Symbol...) + try + is_static_setting(key_path...) && + return getobjpath(load(manager).static_settings, key_path...) + return getobjpath(load(manager).current_settings, key_path...) + catch e + e isa FieldError ? nothing : rethrow() end - return nothing end function fix_static_settings!(manager::ConfigManager) store!(manager) do old_data - new_static = get_static_settings(old_data) - new_data = ConfigManagerData(new_static, old_data.watched_files) + new_static = get_current_settings(old_data.watched_files) + new_data = ConfigManagerData(old_data.current_settings, new_static, old_data.watched_files) return new_data, new_static end end @@ -235,7 +198,7 @@ If the file does not exist or cannot be parsed, just return leaving the current configuration unchanged. When there are unknown keys in the config file, send error message while leaving current configuration unchanged. """ -function load_config!(on_leaf, server::Server, filepath::AbstractString; +function load_config!(callback, server::Server, filepath::AbstractString; reload::Bool = false) store!(server.state.config_manager) do old_data if reload @@ -247,43 +210,46 @@ function load_config!(on_leaf, server::Server, filepath::AbstractString; parsed = TOML.tryparsefile(filepath) parsed isa TOML.ParserError && return old_data, nothing - new_config = to_config_dict(parsed) - - unknown_keys = collect_unmatched_keys(new_config) - if !isempty(unknown_keys) + new_config = try + Configurations.from_dict(JETLSConfig, parsed) + catch e + # TODO: remove this when Configurations.jl support to report + # full path of unknown key. + if e isa Configurations.InvalidKeyError + config_dict = to_config_dict(parsed) + unknown_keys = collect_unmatched_keys(config_dict) + if !isempty(unknown_keys) + show_error_message(server, """ + Configuration file at $filepath contains unknown keys: + $(join(map(x -> string('`', join(x, "."), '`'), unknown_keys), ", ")) + """) + return old_data, nothing + end + end show_error_message(server, """ - Configuration file at $filepath contains unknown keys: - $(join(map(x -> string('`', join(x, "."), '`'), unknown_keys), ", ")) + Failed to load configuration file at $filepath: + $(e) """) return old_data, nothing end - current_config = get(old_data.watched_files, filepath, DEFAULT_CONFIG) - merged_config = traverse_merge(current_config, new_config) do filepath, v - on_leaf(current_config, filepath, v) - v - end new_watched_files = copy(old_data.watched_files) - new_watched_files[filepath] = merged_config - new_data = ConfigManagerData(old_data.static_settings, new_watched_files) + new_watched_files[filepath] = new_config + new_current_settings = get_current_settings(new_watched_files) + on_difference(callback, old_data.current_settings, new_current_settings) + new_data = ConfigManagerData(new_current_settings, old_data.static_settings, new_watched_files) return new_data, nothing end end -to_config_dict(dict::Dict{String,Any}) = ConfigDict( - (k => (v isa Dict{String,Any} ? to_config_dict(v) : v) for (k, v) in dict)...) - -function delete_config!(on_leaf, manager::ConfigManager, filepath::AbstractString) +function delete_config!(callback, manager::ConfigManager, filepath::AbstractString) store!(manager) do old_data - old_settings = get_settings(old_data) + haskey(old_data.watched_files, filepath) || return old_data, nothing new_watched_files = copy(old_data.watched_files) delete!(new_watched_files, filepath) - new_data = ConfigManagerData(old_data.static_settings, new_watched_files) - new_settings = get_settings(new_data) - traverse_merge(old_settings, new_settings) do path, v - on_leaf(old_settings, path, v) - nothing - end + new_current_settings = get_current_settings(new_watched_files) + new_data = ConfigManagerData(new_current_settings, old_data.static_settings, new_watched_files) + on_difference(callback, old_data.current_settings, new_current_settings) return new_data, nothing end end diff --git a/src/did-change-watched-files.jl b/src/did-change-watched-files.jl index 37a786f17..faad3d625 100644 --- a/src/did-change-watched-files.jl +++ b/src/did-change-watched-files.jl @@ -26,39 +26,33 @@ function handle_config_file_change!( changed_static_settings = String[] if change_type == FileChangeType.Created - load_config!(server, changed_path) do current_config, path, new_value - current_value = access_nested_dict(current_config, path...) - if current_value != new_value - if is_static_setting(path...) - push!(changed_static_settings, join(path, ".")) - else - push!(changed_settings, join(path, ".")) - end + load_config!(server, changed_path) do old_val, new_val, path + if is_static_setting(path...) + push!(changed_static_settings, join(path, ".")) + else + push!(changed_settings, join(path, ".")) end + new_val end kind = "Created" elseif change_type == FileChangeType.Changed - load_config!(server, changed_path; reload=true) do current_config, path, new_value - current_value = access_nested_dict(current_config, path...) - if current_value != new_value - if is_static_setting(path...) - push!(changed_static_settings, join(path, ".")) - else - push!(changed_settings, join(path, ".")) - end + load_config!(server, changed_path; reload=true) do old_val, new_val, path + if is_static_setting(path...) + push!(changed_static_settings, join(path, ".")) + else + push!(changed_settings, join(path, ".")) end + new_val end kind = "Updated" elseif change_type == FileChangeType.Deleted - delete_config!(server.state.config_manager, changed_path) do old_config, path, new_value - old_value = access_nested_dict(old_config, path...) - if old_value != new_value - if is_static_setting(path...) - push!(changed_static_settings, join(path, ".")) - else - push!(changed_settings, join(path, ".")) - end + delete_config!(server.state.config_manager, changed_path) do old_val, new_val, path + if is_static_setting(path...) + push!(changed_static_settings, join(path, ".")) + else + push!(changed_settings, join(path, ".")) end + new_val end kind = "Deleted" else error("Unknown FileChangeType") end diff --git a/src/formatting.jl b/src/formatting.jl index 9014dcea3..1fbc88ef0 100644 --- a/src/formatting.jl +++ b/src/formatting.jl @@ -114,9 +114,9 @@ end function format_result(state::ServerState, uri::URI) fi = @something get_file_info(state, uri) return file_cache_error(uri) - setting_path = ("formatter", "runic", "executable") + setting_path = (:formatter, :runic, :executable) executable = get_config(state.config_manager, setting_path...) - default_executable = access_nested_dict(DEFAULT_CONFIG, setting_path...) + default_executable = get_default_config(setting_path...) additional_msg = if executable == default_executable install_instruction_message(executable, RUNIC_INSTALLATION_URL) else @@ -187,9 +187,9 @@ end function range_format_result(state::ServerState, uri::URI, range::Range) fi = @something get_file_info(state, uri) return file_cache_error(uri) - setting_path = ("formatter", "runic", "executable") + setting_path = (:formatter, :runic, :executable) executable = get_config(state.config_manager, setting_path...) - default_executable = access_nested_dict(DEFAULT_CONFIG, setting_path...) + default_executable = get_default_config(setting_path...) additional_msg = if executable == default_executable install_instruction_message(executable, RUNIC_INSTALLATION_URL) else diff --git a/src/testrunner/testrunner.jl b/src/testrunner/testrunner.jl index 7900b037f..bc5f31258 100644 --- a/src/testrunner/testrunner.jl +++ b/src/testrunner/testrunner.jl @@ -444,10 +444,10 @@ function testrunner_run_testset( server::Server, uri::URI, fi::FileInfo, idx::Int, tsn::String, filepath::String; cancellable_token::Union{Nothing,CancellableToken} = nothing ) - setting_path = ("testrunner", "executable") + setting_path = (:testrunner, :executable) executable = get_config(server.state.config_manager, setting_path...) if isnothing(Sys.which(executable)) - default_executable = access_nested_dict(DEFAULT_CONFIG, setting_path...) + default_executable = get_default_config(setting_path...) additional_msg = if executable == default_executable install_instruction_message(executable, TESTRUNNER_INSTALLATION_URL) else @@ -584,10 +584,10 @@ function testrunner_run_testcase( server::Server, uri::URI, tcl::Int, tct::String, filepath::String; cancellable_token::Union{Nothing,CancellableToken} = nothing ) - setting_path = ("testrunner", "executable") + setting_path = (:testrunner, :executable) executable = get_config(server.state.config_manager, setting_path...) if isnothing(Sys.which(executable)) - default_executable = access_nested_dict(DEFAULT_CONFIG, setting_path...) + default_executable = get_default_config(setting_path...) additional_msg = if executable == default_executable install_instruction_message(executable, TESTRUNNER_INSTALLATION_URL) else diff --git a/src/types.jl b/src/types.jl index 57247ff5c..4895ba82c 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,3 +1,5 @@ +using Configurations + const SyntaxTree0 = typeof(JS.build_tree(JL.SyntaxTree, JS.parse!(JS.ParseStream("")))) struct FileInfo @@ -293,13 +295,94 @@ struct LSPostProcessor end LSPostProcessor() = LSPostProcessor(JET.PostProcessor()) -const ConfigDict = Base.PersistentDict{String, Any} +# To extend configuration options, define new `@option struct`s here: +# +# @option struct NewConfig <: ConfigSection +# field1::Maybe{Type1} # Maybe{T} from Configurations.jl allows optional fields +# field2::Maybe{Type2} +# end +# +# All fields must be wrapped in `Maybe{}` to support partial configuration. +# +# Then, update the following methods properly to make the +# `is_static_setting(::Type{JETLSConfig}, field::Symbol)` and +# `default_config(::Type{JETLSConfig}) -> NewConfig` work correctly: +# +# - is_static_setting(::Type{NewConfig}, field::Symbol) -> Bool +# Returns whether a setting requires server restart. +# - default_config(::Type{NewConfig}) -> NewConfig +# Returns the default configuration values. +# +# Finally, add the new config section to `JETLSConfig` struct below. + +abstract type ConfigSection end + +_unwrap_maybe(::Type{Maybe{S}}) where {S} = S +_unwrap_maybe(::Type{T}) where {T} = T + +@option struct FullAnalysisConfig <: ConfigSection + debounce::Maybe{Float64} +end + +is_static_setting(::Type{FullAnalysisConfig}, ::Symbol) = false +default_config(::Type{FullAnalysisConfig}) = FullAnalysisConfig(1.0) + +@option struct TestRunnerConfig <: ConfigSection + executable::Maybe{String} +end + +is_static_setting(::Type{TestRunnerConfig}, ::Symbol) = false +default_config(::Type{TestRunnerConfig}) = TestRunnerConfig(@static Sys.iswindows() ? "testrunner.bat" : "testrunner") + +@option struct RunicConfig <: ConfigSection + executable::Maybe{String} +end + +is_static_setting(::Type{RunicConfig}, ::Symbol) = false +default_config(::Type{RunicConfig}) = RunicConfig(@static Sys.iswindows() ? "runic.bat" : "runic") + +@option struct FormatterConfig <: ConfigSection + runic::Maybe{RunicConfig} +end + +default_config(::Type{FormatterConfig}) = FormatterConfig(default_config(RunicConfig)) + +# configuration item for test purpose +@option struct InternalConfig <: ConfigSection + static_setting::Maybe{Int} + dynamic_setting::Maybe{Int} +end + +is_static_setting(::Type{InternalConfig}, field::Symbol) = field == :static_setting +default_config(::Type{InternalConfig}) = InternalConfig(0, 0) + +@option struct JETLSConfig <: ConfigSection + full_analysis::Maybe{FullAnalysisConfig} + testrunner::Maybe{TestRunnerConfig} + formatter::Maybe{FormatterConfig} + internal::Maybe{InternalConfig} +end + +is_static_setting(path::Symbol...) = + is_static_setting(JETLSConfig, path...) + +is_static_setting(::Type{T}, head::Symbol, rest::Symbol...) where {T<:ConfigSection} = + is_static_setting(_unwrap_maybe(fieldtype(T, head)), rest...) + +const DEFAULT_CONFIG = JETLSConfig( + full_analysis = default_config(FullAnalysisConfig), + testrunner = default_config(TestRunnerConfig), + formatter = default_config(FormatterConfig), + internal = default_config(InternalConfig) +) + +get_default_config(path::Symbol...) = getobjpath(DEFAULT_CONFIG, path...) struct WatchedConfigFiles files::Vector{String} - configs::Vector{ConfigDict} + configs::Vector{JETLSConfig} end -WatchedConfigFiles() = WatchedConfigFiles(String["__DEFAULT_CONFIG__"], ConfigDict[DEFAULT_CONFIG]) +WatchedConfigFiles() = WatchedConfigFiles(String["__DEFAULT_CONFIG__"], [DEFAULT_CONFIG]) Base.copy(watched_files::WatchedConfigFiles) = WatchedConfigFiles(copy(watched_files.files), copy(watched_files.configs)) @@ -326,7 +409,7 @@ function Base.delete!(watched_files::WatchedConfigFiles, file::String) return watched_files end -function Base.setindex!(watched_files::WatchedConfigFiles, config::ConfigDict, file::String) +function Base.setindex!(watched_files::WatchedConfigFiles, config::JETLSConfig, file::String) idx = searchsortedfirst(watched_files.files, file, ConfigFileOrder()) if 1 <= idx <= length(watched_files.files) && watched_files.files[idx] == file watched_files.configs[idx] = config @@ -352,10 +435,11 @@ end struct ConfigFileOrder <: Base.Ordering end struct ConfigManagerData - static_settings::ConfigDict + current_settings::JETLSConfig + static_settings::JETLSConfig watched_files::WatchedConfigFiles end -ConfigManagerData() = ConfigManagerData(ConfigDict(), WatchedConfigFiles()) +ConfigManagerData() = ConfigManagerData(DEFAULT_CONFIG, DEFAULT_CONFIG, WatchedConfigFiles()) # Type aliases for document-synchronization caches using `SWContainer` (sequential-only updates) const FileCache = SWContainer{Base.PersistentDict{URI,FileInfo}, SWStats} diff --git a/src/utils/general.jl b/src/utils/general.jl index 404c5fff3..6d62a1196 100644 --- a/src/utils/general.jl +++ b/src/utils/general.jl @@ -117,8 +117,11 @@ An accessor primarily written for accessing fields of LSP objects whose fields may be unset (set to `nothing`) depending on client/server capabilities. Traverses the field chain `paths...` of `obj`, and returns `nothing` if any `nothing` field is encountered along the way. + +Note: `@noinline` is used to maximize type stability. This is a temporary workaround and +may become unnecessary with future compiler improvements. """ -function getobjpath(obj, path::Symbol, paths::Symbol...) +@noinline Base.@constprop :aggressive function getobjpath(obj, path::Symbol, paths::Symbol...) nextobj = @something getfield(obj, path) return nothing getobjpath(nextobj, paths...) end diff --git a/test/test_config.jl b/test/test_config.jl index 7a4da9546..df02f36df 100644 --- a/test/test_config.jl +++ b/test/test_config.jl @@ -3,39 +3,6 @@ module test_config using Test using JETLS -const TEST_DICT = JETLS.ConfigDict( - "test_key1" => "test_value1", - "test_key2" => JETLS.ConfigDict( - "nested_key1" => "nested_value1", - "nested_key2" => JETLS.ConfigDict( - "deep_nested_key1" => "deep_nested_value1", - "deep_nested_key2" => "deep_nested_value2" - ) - ) -) - -const TEST_DICT_DIFFERENT_VALUE = JETLS.ConfigDict( - "test_key1" => "newvalue_1", # different value (static) - "test_key2" => JETLS.ConfigDict( - "nested_key1" => "nested_value1", - "nested_key2" => JETLS.ConfigDict( - "deep_nested_key1" => "newvalue_2", # different value (static) - "deep_nested_key2" => "newvalue_3" # different value (dynamic) - ) - ) -) - -const TEST_DICT_DIFFERENT_KEY = JETLS.ConfigDict( - "diffname_1" => "test_value1", # different key name - "test_key2" => JETLS.ConfigDict( - "nested_key1" => "nested_value1", - "diffname_2" => JETLS.ConfigDict( # different key name - "deep_nested_key1" => "deep_nested_value1", - "diffname_3" => "deep_nested_value2" # different key under a different key - ) - ), -) - @testset "WatchedConfigFiles" begin @testset "constructor and basic operations" begin watched = JETLS.WatchedConfigFiles() @@ -46,7 +13,7 @@ const TEST_DICT_DIFFERENT_KEY = JETLS.ConfigDict( @testset "setindex! and getindex" begin watched = JETLS.WatchedConfigFiles() - config = JETLS.ConfigDict("key1" => "value1") + config = JETLS.JETLSConfig() watched["/project/.JETLSConfig.toml"] = config @test length(watched) == 2 @@ -58,7 +25,7 @@ const TEST_DICT_DIFFERENT_KEY = JETLS.ConfigDict( @testset "haskey and get" begin watched = JETLS.WatchedConfigFiles() - config = JETLS.ConfigDict("key" => "value") + config = JETLS.JETLSConfig() @test !haskey(watched, "___UNDEFINED___") @test get(watched, "___UNDEFINED___", "default") == "default" @@ -70,7 +37,7 @@ const TEST_DICT_DIFFERENT_KEY = JETLS.ConfigDict( @testset "delete!" begin watched = JETLS.WatchedConfigFiles() - config = JETLS.ConfigDict("key1" => "value1") + config = JETLS.JETLSConfig() watched["/project/.JETLSConfig.toml"] = config @test length(watched) == 2 @@ -93,7 +60,7 @@ const TEST_DICT_DIFFERENT_KEY = JETLS.ConfigDict( @testset "priority with multiple config files" begin watched = JETLS.WatchedConfigFiles() - watched["/home/user/.JETLSConfig.toml"] = JETLS.ConfigDict("source" => "home") + watched["/home/user/.JETLSConfig.toml"] = JETLS.JETLSConfig() files = collect(keys(watched)) # Should be sorted by ConfigFileOrder @@ -105,12 +72,67 @@ const TEST_DICT_DIFFERENT_KEY = JETLS.ConfigDict( end @testset "Configuration utilities" begin + @testset "`get_default_config`" begin + @test JETLS.get_default_config(:testrunner, :executable) == + (@static Sys.iswindows() ? "testrunner.bat" : "testrunner") + @test JETLS.get_default_config(:formatter, :runic, :executable) == + (@static Sys.iswindows() ? "runic.bat" : "runic") + + @test_throws FieldError JETLS.get_default_config(:nonexistent) + @test_throws FieldError JETLS.get_default_config(:full_analysis, :nonexistent) + end + @testset "`is_static_setting`" begin - @test !JETLS.is_static_setting("full_analysis", "debounce") - @test JETLS.is_static_setting("internal", "static_setting") - @test !JETLS.is_static_setting("testrunner", "executable") - @test !JETLS.is_static_setting("nonexistent") - @test !JETLS.is_static_setting("full_analysis", "nonexistent") + @test !JETLS.is_static_setting(:internal, :dynamic_setting) + @test JETLS.is_static_setting(:internal, :static_setting) + @test !JETLS.is_static_setting(:testrunner, :executable) + @test !JETLS.is_static_setting(:formatter, :runic, :executable) + end + + @testset "`merge_setting`" begin + base_config = JETLS.JETLSConfig(; + full_analysis=JETLS.FullAnalysisConfig(1.0), + testrunner=JETLS.TestRunnerConfig("base_runner"), + internal=JETLS.InternalConfig(10, 20) + ) + + overlay_config = JETLS.JETLSConfig(; + full_analysis=JETLS.FullAnalysisConfig(2.0), + testrunner=nothing, + internal=JETLS.InternalConfig(30, nothing) + ) + + merged = JETLS.merge_setting(base_config, overlay_config) + + @test JETLS.getobjpath(merged, :full_analysis, :debounce) == 2.0 + @test JETLS.getobjpath(merged, :testrunner, :executable) == "base_runner" + @test JETLS.getobjpath(merged, :internal, :static_setting) == 30 + @test JETLS.getobjpath(merged, :internal, :dynamic_setting) == 20 + end + + @testset "`on_difference`" begin + let config1 = JETLS.JETLSConfig(; + full_analysis=JETLS.FullAnalysisConfig(1.0), + testrunner=JETLS.TestRunnerConfig("runner1"), + internal=JETLS.InternalConfig(1, 1) + ) + config2 = JETLS.JETLSConfig(; + full_analysis=JETLS.FullAnalysisConfig(2.0), + testrunner=JETLS.TestRunnerConfig("runner2"), + internal=nothing + ) + paths_called = [] + result = JETLS.on_difference(config1, config2) do old_val, new_val, path + push!(paths_called, path) + new_val + end + @test Set(paths_called) == Set([ + (:full_analysis, :debounce), + (:testrunner, :executable), + (:internal, :static_setting), + (:internal, :dynamic_setting) # even though new_val is nothing, track the path + ]) + end end @testset "`is_config_file`" begin @@ -122,21 +144,6 @@ end @test !JETLS.is_config_file("/path/to/regular.txt") end - @testset "`merge_static_settings`" begin - dict1 = JETLS.ConfigDict( - "internal" => JETLS.ConfigDict("static_setting" => 42)) - dict2 = JETLS.ConfigDict( - "internal" => JETLS.ConfigDict("static_setting" => 99), - "testrunner" => JETLS.ConfigDict("executable" => "testrunner2")) - - result = JETLS.merge_static_settings(dict1, dict2) - - # Should merge static keys only - @test result["internal"]["static_setting"] == 99 - # testrunner.executable should NOT be merged (it's false in STATIC_CONFIG) - @test !haskey(result, "testrunner") - end - @testset "config files priority" begin files = ["/foo/bar/.JETLSConfig.toml", "__DEFAULT_CONFIG__"] @test sort!(files, order=JETLS.ConfigFileOrder()) == [ @@ -147,22 +154,27 @@ end end @testset "ConfigDict utilities" begin - @testset "access_nested_dict" begin - @test JETLS.access_nested_dict(TEST_DICT, "test_key1") == "test_value1" - @test JETLS.access_nested_dict(TEST_DICT, "test_key2", "nested_key1") == "nested_value1" - @test JETLS.access_nested_dict(TEST_DICT, "test_key2", "nested_key2", "deep_nested_key1") == "deep_nested_value1" - @test JETLS.access_nested_dict(TEST_DICT, "test_key2", "nested_key2", "deep_nested_key3") === nothing - @test JETLS.access_nested_dict(TEST_DICT, "non_existent_key") === nothing - - let empty_dict = JETLS.ConfigDict() - @test JETLS.access_nested_dict(empty_dict, "key") === nothing - end + TEST_DICT = JETLS.ConfigDict( + "test_key1" => "test_value1", + "test_key2" => JETLS.ConfigDict( + "nested_key1" => "nested_value1", + "nested_key2" => JETLS.ConfigDict( + "deep_nested_key1" => "deep_nested_value1", + "deep_nested_key2" => "deep_nested_value2" + ) + ) + ) - let dict = JETLS.ConfigDict("scalar" => "value") - @test JETLS.access_nested_dict(dict, "scalar", "unknown") === nothing - @test JETLS.access_nested_dict(dict, "scalar") == "value" - end - end + TEST_DICT_DIFFERENT_KEY = JETLS.ConfigDict( + "diffname_1" => "test_value1", + "test_key2" => JETLS.ConfigDict( + "nested_key1" => "nested_value1", + "diffname_2" => JETLS.ConfigDict( + "deep_nested_key1" => "deep_nested_value1", + "diffname_3" => "deep_nested_value2" + ) + ), + ) @testset "collect_unmatched_keys" begin # It is correct that `test_key2.diffname_2.diffname_3` is not included, @@ -173,132 +185,19 @@ end ]) @test isempty(JETLS.collect_unmatched_keys(TEST_DICT, TEST_DICT)) - @test isempty(JETLS.collect_unmatched_keys(TEST_DICT, TEST_DICT_DIFFERENT_VALUE)) - # single-arg version should use DEFAULT_CONFIG + # single-arg version should use DEFAULT_CONFIG_DICT @test JETLS.collect_unmatched_keys(TEST_DICT_DIFFERENT_KEY) == - JETLS.collect_unmatched_keys(TEST_DICT_DIFFERENT_KEY, JETLS.DEFAULT_CONFIG) - end - - @testset "`traverse_merge`" begin - # basic merge with custom on_leaf function - let target = JETLS.ConfigDict("a" => 1) - source = JETLS.ConfigDict("b" => 2) - collected_calls = Tuple{Vector{String},Any}[] - result = JETLS.traverse_merge(target, source) do path, v - push!(collected_calls, (path, v)) - v * 10 - end - @test result == JETLS.ConfigDict("a" => 1, "b" => 20) - @test length(collected_calls) == 1 - @test collected_calls[1][1] == ["b"] - @test collected_calls[1][2] == 2 - end - - # nested merge with custom on_leaf - let target = JETLS.ConfigDict("nested" => JETLS.ConfigDict("a" => 1)) - source = JETLS.ConfigDict("nested" => JETLS.ConfigDict("b" => 2)) - collected_paths = Vector{String}[] - result = JETLS.traverse_merge(target, source) do path, v - push!(collected_paths, path) - v - end - @test result == JETLS.ConfigDict("nested" => JETLS.ConfigDict("a" => 1, "b" => 2)) - @test collected_paths == [["nested", "b"]] - end - - # creating new nested structure - let target = JETLS.ConfigDict() - source = JETLS.ConfigDict("new" => JETLS.ConfigDict("deep" => "value")) - result = JETLS.traverse_merge(target, source) do _, v - v - end - @test result == JETLS.ConfigDict("new" => JETLS.ConfigDict("deep" => "value")) - end - - # overwriting non-dict with dict - let target = JETLS.ConfigDict("key" => "scalar") - source = JETLS.ConfigDict("key" => JETLS.ConfigDict("nested" => "value")) - result = JETLS.traverse_merge(target, source) do _, v - v - end - @test result == JETLS.ConfigDict("key" => JETLS.ConfigDict("nested" => "value")) - end - - # filtering with on_leaf returning nothing - let base = JETLS.ConfigDict("a" => 1, "b" => 2) - overlay = JETLS.ConfigDict("b" => 3, "c" => 4) - result = JETLS.traverse_merge(base, overlay) do path, v - path[end] == "b" ? v : nothing # only merge keys ending with "b" - end - @test result == JETLS.ConfigDict("a" => 1, "b" => 3) # only "b" was merged - end - - # filtering with nested paths - let base = JETLS.ConfigDict("full_analysis" => JETLS.ConfigDict("debounce" => 1.0)) - overlay = JETLS.ConfigDict("full_analysis" => JETLS.ConfigDict("debounce" => 5.0)) - result = JETLS.traverse_merge(base, overlay) do path, v - path == ["full_analysis", "debounce"] ? v : nothing - end - @test result["full_analysis"]["debounce"] == 5.0 - end - end - - @testset "`cleanup_empty_dicts`" begin - let dict = JETLS.ConfigDict( - "keep" => "value", - "empty_nested" => JETLS.ConfigDict(), - "nested" => JETLS.ConfigDict( - "keep_nested" => "value", - "empty_deep" => JETLS.ConfigDict( - "empty_deeper" => JETLS.ConfigDict() - ) - ) - ) - result = JETLS.cleanup_empty_dicts(dict) - @test result == JETLS.ConfigDict( - "keep" => "value", - "nested" => JETLS.ConfigDict("keep_nested" => "value") - ) - end - - # completely empty dict should remain empty - let dict = JETLS.ConfigDict() - result = JETLS.cleanup_empty_dicts(dict) - @test result == JETLS.ConfigDict() - end - - # dict with only empty nested dicts should become empty - let dict = JETLS.ConfigDict("empty" => JETLS.ConfigDict()) - result = JETLS.cleanup_empty_dicts(dict) - @test result == JETLS.ConfigDict() - end - - let dict = JETLS.ConfigDict( - "nested" => JETLS.ConfigDict( - "nested2 " => JETLS.ConfigDict( - "deep_nested" => 1 - ) - ) - ) - result = JETLS.cleanup_empty_dicts(dict) - @test result == JETLS.ConfigDict( - "nested" => JETLS.ConfigDict( - "nested2 " => JETLS.ConfigDict( - "deep_nested" => 1 - ) - ) - ) - end + JETLS.collect_unmatched_keys(TEST_DICT_DIFFERENT_KEY, JETLS.DEFAULT_CONFIG_DICT) end end -# Test utility for setting config files in ConfigManager -function storeconfig!(manager::JETLS.ConfigManager, filepath::AbstractString, new_config::JETLS.ConfigDict) +function storeconfig!(manager::JETLS.ConfigManager, filepath::AbstractString, new_config::JETLS.JETLSConfig) JETLS.store!(manager) do old_data new_watched_files = copy(old_data.watched_files) new_watched_files[filepath] = new_config - new_data = JETLS.ConfigManagerData(old_data.static_settings, new_watched_files) + new_current_settings = JETLS.get_current_settings(new_watched_files) + new_data = JETLS.ConfigManagerData(new_current_settings, old_data.static_settings, new_watched_files) return new_data, nothing end end @@ -306,80 +205,72 @@ end @testset "ConfigManager" begin manager = JETLS.ConfigManager(JETLS.ConfigManagerData()) - test_config = JETLS.ConfigDict( - "full_analysis" => JETLS.ConfigDict( - "debounce" => 2.0 - ), - "testrunner" => JETLS.ConfigDict( - "executable" => "test_runner" - ), - "internal" => JETLS.ConfigDict( - "static_setting" => 5 - ) + test_config = JETLS.JETLSConfig(; + full_analysis=JETLS.FullAnalysisConfig(2.0), + testrunner=JETLS.TestRunnerConfig("test_runner"), + internal=JETLS.InternalConfig(5, nothing) ) storeconfig!(manager, "/foo/bar/.JETLSConfig.toml", test_config) JETLS.fix_static_settings!(manager) - @test JETLS.get_config(manager, "full_analysis", "debounce") === 2.0 - @test JETLS.get_config(manager, "testrunner", "executable") === "test_runner" - @test JETLS.get_config(manager, "non_existent_key") === nothing + @test JETLS.get_config(manager, :full_analysis, :debounce) === 2.0 + @test JETLS.get_config(manager, :testrunner, :executable) === "test_runner" + @test JETLS.get_config(manager, :non_existent_key) === nothing + + @test Base.infer_return_type((typeof(manager),)) do manager + JETLS.get_config(manager, :internal, :dynamic_setting) + end == Union{Nothing, Int} + + @test Base.infer_return_type((typeof(manager),)) do manager + JETLS.get_config(manager, :internal, :static_setting) + end == Union{Nothing, Int} # Test priority: __DEFAULT_CONFIG__ has lower priority - override_config = JETLS.ConfigDict( - "full_analysis" => JETLS.ConfigDict( - "debounce" => 999.0 - ), - "testrunner" => JETLS.ConfigDict( - "executable" => "override_runner" - ) + override_config = JETLS.JETLSConfig(; + full_analysis=JETLS.FullAnalysisConfig(999.0), + testrunner=JETLS.TestRunnerConfig("override_runner") ) storeconfig!(manager, "__DEFAULT_CONFIG__", override_config) # High priority config should still win - @test JETLS.get_config(manager, "full_analysis", "debounce") === 2.0 - @test JETLS.get_config(manager, "testrunner", "executable") === "test_runner" + @test JETLS.get_config(manager, :full_analysis, :debounce) === 2.0 + @test JETLS.get_config(manager, :testrunner, :executable) === "test_runner" # Test updating config changed_static_keys = Set{String}() - updated_config = JETLS.ConfigDict( - "full_analysis" => JETLS.ConfigDict( - "debounce" => 3.0 - ), - "testrunner" => JETLS.ConfigDict( - "executable" => "new_runner" # not static - ), - "internal" => JETLS.ConfigDict( - "static_setting" => 10 - ) + updated_config = JETLS.JETLSConfig(; + full_analysis=JETLS.FullAnalysisConfig(3.0), + testrunner=JETLS.TestRunnerConfig("new_runner"), + internal=JETLS.InternalConfig(10, nothing) ) - merged_config = let data = JETLS.load(manager) + let data = JETLS.load(manager) current_config = get(data.watched_files, "/foo/bar/.JETLSConfig.toml", JETLS.DEFAULT_CONFIG) - JETLS.traverse_merge(current_config, updated_config) do path, v - if JETLS.is_static_setting(path...) + JETLS.on_difference(current_config, updated_config) do old_val, new_val, path + if JETLS.is_static_setting(JETLS.JETLSConfig, path...) push!(changed_static_keys, join(path, ".")) end - v + return new_val end end - storeconfig!(manager, "/foo/bar/.JETLSConfig.toml", merged_config) + storeconfig!(manager, "/foo/bar/.JETLSConfig.toml", updated_config) # `on_static_setting` should be called for static keys @test changed_static_keys == Set(["internal.static_setting"]) # non static keys should be changed dynamically - @test JETLS.get_config(manager, "testrunner", "executable") == "new_runner" - @test JETLS.get_config(manager, "full_analysis", "debounce") == 3.0 + @test JETLS.get_config(manager, :testrunner, :executable) == "new_runner" + @test JETLS.get_config(manager, :full_analysis, :debounce) == 3.0 # static keys should NOT change (they stay at the fixed values) - @test JETLS.get_config(manager, "internal", "static_setting") == 5 + @test JETLS.get_config(manager, :internal, :static_setting) == 5 end @testset "`fix_static_settings!`" begin manager = JETLS.ConfigManager(JETLS.ConfigManagerData()) - high_priority_config = JETLS.ConfigDict( - "internal" => JETLS.ConfigDict("static_setting" => 2) + high_priority_config = JETLS.JETLSConfig(; + internal=JETLS.InternalConfig(2, nothing) ) - low_priority_config = JETLS.ConfigDict( - "internal" => JETLS.ConfigDict("static_setting" => 999), - "testrunner" => JETLS.ConfigDict("executable" => "custom") + low_priority_config = JETLS.JETLSConfig(; + internal=JETLS.InternalConfig(999, nothing), + testrunner=JETLS.TestRunnerConfig("custom") ) storeconfig!(manager, "__DEFAULT_CONFIG__", low_priority_config) @@ -388,9 +279,7 @@ end # high priority should win for the static keys data = JETLS.load(manager) - @test data.static_settings["internal"]["static_setting"] == 2 - # testrunner.executable is not static, so should not be in `static_settings` - @test !haskey(data.static_settings, "testrunner") + @test JETLS.getobjpath(data.static_settings, :internal, :static_setting) == 2 end end # test_config diff --git a/test/test_did-change-watched-files.jl b/test/test_did-change-watched-files.jl index ff44b64b9..12d859717 100644 --- a/test/test_did-change-watched-files.jl +++ b/test/test_did-change-watched-files.jl @@ -15,11 +15,11 @@ const CLIENT_CAPABILITIES = ClientCapabilities( ) ) -const DEBOUNCE_DEFAULT = JETLS.access_nested_dict(JETLS.DEFAULT_CONFIG, - "full_analysis", "debounce") +const DEBOUNCE_DEFAULT = JETLS.getobjpath(JETLS.DEFAULT_CONFIG, + :full_analysis, :debounce) -const STATIC_SETTING_DEFAULT = JETLS.access_nested_dict(JETLS.DEFAULT_CONFIG, - "internal", "static_setting") +const STATIC_SETTING_DEFAULT = JETLS.getobjpath(JETLS.DEFAULT_CONFIG, + :internal, :static_setting) # Test the full cycle of `DidChangeWatchedFilesNotification`: # 1. Initialize with `.JETLSConfig.toml` file. @@ -44,8 +44,8 @@ const STATIC_SETTING_DEFAULT = JETLS.access_nested_dict(JETLS.DEFAULT_CONFIG, withserver(; rootUri, capabilities=CLIENT_CAPABILITIES) do (; writereadmsg, server) manager = server.state.config_manager - @test JETLS.get_config(manager, "internal", "static_setting") == STATIC_SETTING_STARTUP - @test JETLS.get_config(manager, "testrunner", "executable") == TESTRUNNER_STARTUP + @test JETLS.get_config(manager, :internal, :static_setting) == STATIC_SETTING_STARTUP + @test JETLS.get_config(manager, :testrunner, :executable) == TESTRUNNER_STARTUP # change `internal.static_setting` to `STATIC_SETTING_V2` STATIC_SETTING_V2 = 200 @@ -69,7 +69,7 @@ const STATIC_SETTING_DEFAULT = JETLS.access_nested_dict(JETLS.DEFAULT_CONFIG, end # Static setting should not be changed - @test JETLS.get_config(manager, "internal", "static_setting") == STATIC_SETTING_STARTUP + @test JETLS.get_config(manager, :internal, :static_setting) == STATIC_SETTING_STARTUP DEBOUNCE_V2 = 300.0 # Add a new key `full_analysis.debounce` (now dynamic) @@ -96,7 +96,7 @@ const STATIC_SETTING_DEFAULT = JETLS.access_nested_dict(JETLS.DEFAULT_CONFIG, end # `full_analysis.debounce` should be changed (dynamic) - @test JETLS.get_config(manager, "full_analysis", "debounce") == DEBOUNCE_V2 + @test JETLS.get_config(manager, :full_analysis, :debounce) == DEBOUNCE_V2 # Change `testrunner.executable` to "newtestrunner" TESTRUNNER_V2 = "testrunner_v2" @@ -124,7 +124,7 @@ const STATIC_SETTING_DEFAULT = JETLS.access_nested_dict(JETLS.DEFAULT_CONFIG, end # testrunner.executable should be updated in both configs (dynamic) - @test JETLS.get_config(manager, "testrunner", "executable") == TESTRUNNER_V2 + @test JETLS.get_config(manager, :testrunner, :executable) == TESTRUNNER_V2 # unknown keys should be reported write(config_path, """ @@ -159,9 +159,9 @@ const STATIC_SETTING_DEFAULT = JETLS.access_nested_dict(JETLS.DEFAULT_CONFIG, # After deletion, # - For static keys, `get_config` should remain unchanged - @test JETLS.get_config(manager, "internal", "static_setting") == STATIC_SETTING_STARTUP + @test JETLS.get_config(manager, :internal, :static_setting) == STATIC_SETTING_STARTUP # - For non-static keys, replace with value from the next highest-priority config file. (`__DEFAULT_CONFIG__`) - @test JETLS.get_config(manager, "full_analysis", "debounce") == DEBOUNCE_DEFAULT + @test JETLS.get_config(manager, :full_analysis, :debounce) == DEBOUNCE_DEFAULT # re-create the config file STATIC_SETTING_RECREATE = 400 @@ -186,7 +186,7 @@ const STATIC_SETTING_DEFAULT = JETLS.access_nested_dict(JETLS.DEFAULT_CONFIG, end # static keys should not be changed even if higher priority config file is re-created - @test JETLS.get_config(manager, "internal", "static_setting") == STATIC_SETTING_STARTUP + @test JETLS.get_config(manager, :internal, :static_setting) == STATIC_SETTING_STARTUP # non-config file change (should be ignored) other_file = joinpath(tmpdir, "other.txt") @@ -199,8 +199,8 @@ const STATIC_SETTING_DEFAULT = JETLS.access_nested_dict(JETLS.DEFAULT_CONFIG, writereadmsg(msg; read=0) end # no effect on config - @test JETLS.get_config(manager, "internal", "static_setting") == STATIC_SETTING_STARTUP - @test JETLS.get_config(manager, "testrunner", "executable") == TESTRUNNER_RECREATE + @test JETLS.get_config(manager, :internal, :static_setting) == STATIC_SETTING_STARTUP + @test JETLS.get_config(manager, :testrunner, :executable) == TESTRUNNER_RECREATE end end end @@ -234,9 +234,9 @@ end # If higher priority config file is created, # - static keys should not be changed - @test JETLS.get_config(manager, "internal", "static_setting") == STATIC_SETTING_DEFAULT + @test JETLS.get_config(manager, :internal, :static_setting) == STATIC_SETTING_DEFAULT # - non-static keys should be updated - @test JETLS.get_config(manager, "testrunner", "executable") == TESTRUNNER_RECREATE + @test JETLS.get_config(manager, :testrunner, :executable) == TESTRUNNER_RECREATE # New config file change also should be watched STATIC_SETTING_V2 = 600 @@ -259,8 +259,8 @@ end @test occursin("Updated", raw_res.params.message) @test occursin("`internal.static_setting`", raw_res.params.message) @test occursin("restart", raw_res.params.message) - @test JETLS.get_config(manager, "internal", "static_setting") == STATIC_SETTING_DEFAULT - @test JETLS.get_config(manager, "testrunner", "executable") == TESTRUNNER_V2 + @test JETLS.get_config(manager, :internal, :static_setting) == STATIC_SETTING_DEFAULT + @test JETLS.get_config(manager, :testrunner, :executable) == TESTRUNNER_V2 end end end