NickelEval.jl

Julia FFI bindings for Nickel configuration language
Log | Files | Refs | README | LICENSE

test_eval.jl (16167B)


      1 @testset "C API Evaluation" begin
      2     @testset "Primitive types" begin
      3         # Integers
      4         @test nickel_eval("42") === Int64(42)
      5         @test nickel_eval("-42") === Int64(-42)
      6         @test nickel_eval("0") === Int64(0)
      7         @test nickel_eval("1000000000000") === Int64(1000000000000)
      8 
      9         # Floats (only true decimals)
     10         @test nickel_eval("3.14") ≈ 3.14
     11         @test nickel_eval("-2.718") ≈ -2.718
     12         @test nickel_eval("0.5") ≈ 0.5
     13         @test typeof(nickel_eval("3.14")) == Float64
     14 
     15         # Booleans
     16         @test nickel_eval("true") === true
     17         @test nickel_eval("false") === false
     18 
     19         # Null
     20         @test nickel_eval("null") === nothing
     21 
     22         # Strings
     23         @test nickel_eval("\"hello\"") == "hello"
     24         @test nickel_eval("\"\"") == ""
     25         @test nickel_eval("\"hello 世界\"") == "hello 世界"
     26     end
     27 
     28     @testset "Arrays" begin
     29         @test nickel_eval("[]") == Any[]
     30         @test nickel_eval("[1, 2, 3]") == Any[1, 2, 3]
     31         @test nickel_eval("[true, false]") == Any[true, false]
     32         @test nickel_eval("[\"a\", \"b\"]") == Any["a", "b"]
     33 
     34         # Nested arrays
     35         result = nickel_eval("[[1, 2], [3, 4]]")
     36         @test result == Any[Any[1, 2], Any[3, 4]]
     37 
     38         # Mixed types
     39         result = nickel_eval("[1, \"two\", true, null]")
     40         @test result == Any[1, "two", true, nothing]
     41     end
     42 
     43     @testset "Records" begin
     44         result = nickel_eval("{ x = 1 }")
     45         @test result isa Dict{String, Any}
     46         @test result["x"] === Int64(1)
     47 
     48         result = nickel_eval("{ name = \"test\", count = 42 }")
     49         @test result["name"] == "test"
     50         @test result["count"] === Int64(42)
     51 
     52         # Empty record
     53         @test nickel_eval("{}") == Dict{String, Any}()
     54 
     55         # Nested records
     56         result = nickel_eval("{ outer = { inner = 42 } }")
     57         @test result["outer"]["inner"] === Int64(42)
     58     end
     59 
     60     @testset "Type preservation" begin
     61         @test typeof(nickel_eval("42")) == Int64
     62         @test typeof(nickel_eval("42.5")) == Float64
     63         @test typeof(nickel_eval("42.0")) == Int64  # whole numbers -> Int64
     64     end
     65 
     66     @testset "Computed values" begin
     67         @test nickel_eval("1 + 2") === Int64(3)
     68         @test nickel_eval("10 - 3") === Int64(7)
     69         @test nickel_eval("let x = 10 in x * 2") === Int64(20)
     70         @test nickel_eval("let add = fun x y => x + y in add 3 4") === Int64(7)
     71     end
     72 
     73     @testset "Record operations" begin
     74         result = nickel_eval("{ a = 1 } & { b = 2 }")
     75         @test result["a"] === Int64(1)
     76         @test result["b"] === Int64(2)
     77     end
     78 
     79     @testset "Array operations" begin
     80         result = nickel_eval("[1, 2, 3] |> std.array.map (fun x => x * 2)")
     81         @test result == Any[2, 4, 6]
     82     end
     83 
     84     @testset "Enums - Simple (no argument)" begin
     85         result = nickel_eval("let x = 'Foo in x")
     86         @test result isa NickelEnum
     87         @test result.tag == :Foo
     88         @test result.arg === nothing
     89 
     90         # Convenience comparison
     91         @test result == :Foo
     92         @test :Foo == result
     93         @test result != :Bar
     94 
     95         @test nickel_eval("let x = 'None in x").tag == :None
     96         @test nickel_eval("let x = 'True in x").tag == :True
     97         @test nickel_eval("let x = 'False in x").tag == :False
     98         @test nickel_eval("let x = 'Pending in x").tag == :Pending
     99         @test nickel_eval("let x = 'Red in x").tag == :Red
    100     end
    101 
    102     @testset "Enums - With primitive arguments" begin
    103         # Integer argument
    104         result = nickel_eval("let x = 'Count 42 in x")
    105         @test result.tag == :Count
    106         @test result.arg === Int64(42)
    107 
    108         # Negative integer (needs parentheses in Nickel)
    109         result = nickel_eval("let x = 'Offset (-100) in x")
    110         @test result.arg === Int64(-100)
    111 
    112         # Float argument
    113         result = nickel_eval("let x = 'Temperature 98.6 in x")
    114         @test result.tag == :Temperature
    115         @test result.arg ≈ 98.6
    116 
    117         # String argument
    118         result = nickel_eval("let x = 'Message \"hello world\" in x")
    119         @test result.tag == :Message
    120         @test result.arg == "hello world"
    121 
    122         # Empty string argument
    123         result = nickel_eval("let x = 'Empty \"\" in x")
    124         @test result.arg == ""
    125 
    126         # Boolean arguments
    127         result = nickel_eval("let x = 'Flag true in x")
    128         @test result.arg === true
    129         result = nickel_eval("let x = 'Flag false in x")
    130         @test result.arg === false
    131 
    132         # Null argument
    133         result = nickel_eval("let x = 'Nullable null in x")
    134         @test result.arg === nothing
    135     end
    136 
    137     @testset "Enums - With record arguments" begin
    138         # Simple record argument
    139         result = nickel_eval("let x = 'Ok { value = 123 } in x")
    140         @test result.tag == :Ok
    141         @test result.arg isa Dict{String, Any}
    142         @test result.arg["value"] === Int64(123)
    143 
    144         # Record with multiple fields
    145         code = """
    146         let result = 'Ok { value = 123, message = "success" } in result
    147         """
    148         result = nickel_eval(code)
    149         @test result.arg["value"] === Int64(123)
    150         @test result.arg["message"] == "success"
    151 
    152         # Error with details
    153         code = """
    154         let err = 'Error { code = 404, reason = "not found" } in err
    155         """
    156         result = nickel_eval(code)
    157         @test result.tag == :Error
    158         @test result.arg["code"] === Int64(404)
    159         @test result.arg["reason"] == "not found"
    160 
    161         # Nested record in enum
    162         code = """
    163         let x = 'Data { outer = { inner = 42 } } in x
    164         """
    165         result = nickel_eval(code)
    166         @test result.arg["outer"]["inner"] === Int64(42)
    167     end
    168 
    169     @testset "Enums - With array arguments" begin
    170         # Array of integers
    171         result = nickel_eval("let x = 'Batch [1, 2, 3, 4, 5] in x")
    172         @test result.tag == :Batch
    173         @test result.arg == Any[1, 2, 3, 4, 5]
    174 
    175         # Empty array
    176         result = nickel_eval("let x = 'Empty [] in x")
    177         @test result.arg == Any[]
    178 
    179         # Array of strings
    180         result = nickel_eval("let x = 'Names [\"alice\", \"bob\"] in x")
    181         @test result.arg == Any["alice", "bob"]
    182 
    183         # Array of records
    184         code = """
    185         let x = 'Users [{ name = "alice" }, { name = "bob" }] in x
    186         """
    187         result = nickel_eval(code)
    188         @test result.arg[1]["name"] == "alice"
    189         @test result.arg[2]["name"] == "bob"
    190     end
    191 
    192     @testset "Enums - Nested enums" begin
    193         # Enum inside record inside enum
    194         code = """
    195         let outer = 'Container { inner = 'Value 42 } in outer
    196         """
    197         result = nickel_eval(code)
    198         @test result.tag == :Container
    199         @test result.arg["inner"] isa NickelEnum
    200         @test result.arg["inner"].tag == :Value
    201         @test result.arg["inner"].arg === Int64(42)
    202 
    203         # Array of enums inside enum
    204         code = """
    205         let items = 'List ['Some 1, 'None, 'Some 3] in items
    206         """
    207         result = nickel_eval(code)
    208         @test result.tag == :List
    209         @test length(result.arg) == 3
    210         @test result.arg[1].tag == :Some
    211         @test result.arg[1].arg === Int64(1)
    212         @test result.arg[2].tag == :None
    213         @test result.arg[2].arg === nothing
    214         @test result.arg[3].tag == :Some
    215         @test result.arg[3].arg === Int64(3)
    216 
    217         # Deeply nested enums
    218         code = """
    219         let x = 'L1 { a = 'L2 { b = 'L3 42 } } in x
    220         """
    221         result = nickel_eval(code)
    222         @test result.arg["a"].arg["b"].arg === Int64(42)
    223     end
    224 
    225     @testset "Enums - Pattern matching" begin
    226         # Match resolves to extracted value
    227         code = """
    228         let x = 'Some 42 in
    229         x |> match {
    230           'Some v => v,
    231           'None => 0
    232         }
    233         """
    234         result = nickel_eval(code)
    235         @test result === Int64(42)
    236 
    237         # Match with record destructuring
    238         code = """
    239         let result = 'Ok { value = 100 } in
    240         result |> match {
    241           'Ok r => r.value,
    242           'Error _ => -1
    243         }
    244         """
    245         result = nickel_eval(code)
    246         @test result === Int64(100)
    247 
    248         # Match returning enum
    249         code = """
    250         let x = 'Some 42 in
    251         x |> match {
    252           'Some v => 'Doubled (v * 2),
    253           'None => 'Zero 0
    254         }
    255         """
    256         result = nickel_eval(code)
    257         @test result.tag == :Doubled
    258         @test result.arg === Int64(84)
    259     end
    260 
    261     @testset "Enums - Pretty printing" begin
    262         # Simple enum
    263         @test repr(nickel_eval("let x = 'None in x")) == "'None"
    264         @test repr(nickel_eval("let x = 'Foo in x")) == "'Foo"
    265 
    266         # Enum with simple argument
    267         @test repr(nickel_eval("let x = 'Some 42 in x")) == "'Some 42"
    268 
    269         # Enum with string argument
    270         result = nickel_eval("let x = 'Msg \"hi\" in x")
    271         @test startswith(repr(result), "'Msg")
    272     end
    273 
    274     @testset "Enums - Real-world patterns" begin
    275         # Result type pattern
    276         code = """
    277         let divide = fun a b =>
    278           if b == 0 then
    279             'Err "division by zero"
    280           else
    281             'Ok (a / b)
    282         in
    283         divide 10 2
    284         """
    285         result = nickel_eval(code)
    286         @test result == :Ok
    287         @test result.arg === Int64(5)
    288 
    289         # Option type pattern
    290         code = """
    291         let find = fun arr pred =>
    292           let matches = std.array.filter pred arr in
    293           if std.array.length matches == 0 then
    294             'None
    295           else
    296             'Some (std.array.first matches)
    297         in
    298         find [1, 2, 3, 4] (fun x => x > 2)
    299         """
    300         result = nickel_eval(code)
    301         @test result == :Some
    302         @test result.arg === Int64(3)
    303 
    304         # State machine pattern
    305         code = """
    306         let state = 'Running { progress = 75, task = "downloading" } in state
    307         """
    308         result = nickel_eval(code)
    309         @test result.tag == :Running
    310         @test result.arg["progress"] === Int64(75)
    311         @test result.arg["task"] == "downloading"
    312     end
    313 
    314     @testset "Deeply nested structures" begin
    315         # Deep nesting
    316         result = nickel_eval("{ a = { b = { c = { d = 42 } } } }")
    317         @test result["a"]["b"]["c"]["d"] === Int64(42)
    318 
    319         # Array of records
    320         result = nickel_eval("[{ x = 1 }, { x = 2 }, { x = 3 }]")
    321         @test length(result) == 3
    322         @test result[1]["x"] === Int64(1)
    323         @test result[3]["x"] === Int64(3)
    324 
    325         # Records containing arrays
    326         result = nickel_eval("{ items = [1, 2, 3], name = \"test\" }")
    327         @test result["items"] == Any[1, 2, 3]
    328         @test result["name"] == "test"
    329 
    330         # Mixed deep nesting
    331         result = nickel_eval("{ data = [{ a = 1 }, { b = [true, false] }] }")
    332         @test result["data"][1]["a"] === Int64(1)
    333         @test result["data"][2]["b"] == Any[true, false]
    334     end
    335 
    336     @testset "Typed evaluation - primitives" begin
    337         @test nickel_eval("42", Int) === 42
    338         @test nickel_eval("3.14", Float64) === 3.14
    339         @test nickel_eval("\"hello\"", String) == "hello"
    340         @test nickel_eval("true", Bool) === true
    341     end
    342 
    343     @testset "Typed evaluation - Dict{String, V}" begin
    344         result = nickel_eval("{ a = 1, b = 2 }", Dict{String, Int})
    345         @test result isa Dict{String, Int}
    346         @test result["a"] === 1
    347         @test result["b"] === 2
    348     end
    349 
    350     @testset "Typed evaluation - Dict{Symbol, V}" begin
    351         result = nickel_eval("{ x = 1.5, y = 2.5 }", Dict{Symbol, Float64})
    352         @test result isa Dict{Symbol, Float64}
    353         @test result[:x] === 1.5
    354         @test result[:y] === 2.5
    355     end
    356 
    357     @testset "Typed evaluation - Vector{T}" begin
    358         result = nickel_eval("[1, 2, 3]", Vector{Int})
    359         @test result isa Vector{Int}
    360         @test result == [1, 2, 3]
    361 
    362         result = nickel_eval("[\"a\", \"b\", \"c\"]", Vector{String})
    363         @test result isa Vector{String}
    364         @test result == ["a", "b", "c"]
    365     end
    366 
    367     @testset "Typed evaluation - NamedTuple" begin
    368         result = nickel_eval("{ host = \"localhost\", port = 8080 }",
    369                              @NamedTuple{host::String, port::Int})
    370         @test result isa NamedTuple{(:host, :port), Tuple{String, Int}}
    371         @test result.host == "localhost"
    372         @test result.port === 8080
    373     end
    374 
    375     @testset "String macro" begin
    376         @test ncl"42" === Int64(42)
    377         @test ncl"1 + 1" == 2
    378         @test ncl"true" === true
    379         @test ncl"{ x = 10 }"["x"] === Int64(10)
    380     end
    381 
    382     @testset "check_ffi_available" begin
    383         @test check_ffi_available() === true
    384     end
    385 
    386     @testset "Error handling" begin
    387         # Undefined variable
    388         @test_throws NickelError nickel_eval("undefined_variable")
    389         # Syntax error
    390         @test_throws NickelError nickel_eval("{ x = }")
    391     end
    392 end
    393 
    394 @testset "File Evaluation" begin
    395     mktempdir() do dir
    396         # Simple file
    397         f = joinpath(dir, "test.ncl")
    398         write(f, "{ x = 42 }")
    399         result = nickel_eval_file(f)
    400         @test result["x"] === Int64(42)
    401 
    402         # File returning a primitive
    403         f2 = joinpath(dir, "prim.ncl")
    404         write(f2, "1 + 2")
    405         @test nickel_eval_file(f2) === Int64(3)
    406 
    407         # File with import
    408         shared = joinpath(dir, "shared.ncl")
    409         write(shared, """
    410         {
    411           project_name = "TestProject",
    412           version = "1.0.0"
    413         }
    414         """)
    415         main = joinpath(dir, "main.ncl")
    416         write(main, """
    417 let shared = import "shared.ncl" in
    418 {
    419   name = shared.project_name,
    420   version = shared.version,
    421   extra = "main-specific"
    422 }
    423 """)
    424         result = nickel_eval_file(main)
    425         @test result isa Dict{String, Any}
    426         @test result["name"] == "TestProject"
    427         @test result["version"] == "1.0.0"
    428         @test result["extra"] == "main-specific"
    429 
    430         # Nested imports
    431         utils_file = joinpath(dir, "utils.ncl")
    432         write(utils_file, """
    433         {
    434           helper = fun x => x * 2
    435         }
    436         """)
    437 
    438         complex_file = joinpath(dir, "complex.ncl")
    439         write(complex_file, """
    440 let shared = import "shared.ncl" in
    441 let utils = import "utils.ncl" in
    442 {
    443   project = shared.project_name,
    444   doubled_value = utils.helper 21
    445 }
    446 """)
    447         result = nickel_eval_file(complex_file)
    448         @test result["project"] == "TestProject"
    449         @test result["doubled_value"] === Int64(42)
    450 
    451         # File evaluation with enums
    452         enum_file = joinpath(dir, "enum_config.ncl")
    453         write(enum_file, """
    454         {
    455           status = 'Active,
    456           result = 'Ok 42
    457         }
    458         """)
    459 
    460         result = nickel_eval_file(enum_file)
    461         @test result["status"] isa NickelEnum
    462         @test result["status"] == :Active
    463         @test result["result"].tag == :Ok
    464         @test result["result"].arg === Int64(42)
    465 
    466         # Subdirectory imports
    467         subdir = joinpath(dir, "lib")
    468         mkdir(subdir)
    469         lib_file = joinpath(subdir, "library.ncl")
    470         write(lib_file, """
    471         {
    472           lib_version = "2.0"
    473         }
    474         """)
    475 
    476         with_subdir_file = joinpath(dir, "use_lib.ncl")
    477         write(with_subdir_file, """
    478 let lib = import "lib/library.ncl" in
    479 {
    480   using = lib.lib_version
    481 }
    482 """)
    483         result = nickel_eval_file(with_subdir_file)
    484         @test result["using"] == "2.0"
    485     end
    486 
    487     # Non-existent file
    488     @test_throws NickelError nickel_eval_file("/nonexistent/path/file.ncl")
    489 
    490     # Import not found
    491     mktempdir() do dir
    492         bad_import = joinpath(dir, "bad_import.ncl")
    493         write(bad_import, """
    494 let missing = import "not_there.ncl" in
    495 missing
    496 """)
    497         @test_throws NickelError nickel_eval_file(bad_import)
    498     end
    499 end
    500 
    501 @testset "Export formats" begin
    502     json = nickel_to_json("{ a = 1 }")
    503     @test occursin("\"a\"", json)
    504     @test occursin("1", json)
    505 
    506     yaml = nickel_to_yaml("{ a = 1 }")
    507     @test occursin("a:", yaml)
    508 
    509     toml = nickel_to_toml("{ a = 1 }")
    510     @test occursin("a = 1", toml)
    511 
    512     # Export more complex structures
    513     json2 = nickel_to_json("{ name = \"test\", values = [1, 2, 3] }")
    514     @test occursin("\"name\"", json2)
    515     @test occursin("\"test\"", json2)
    516 
    517     # Export error: expression that can't be evaluated
    518     @test_throws NickelError nickel_to_json("undefined_variable")
    519 end