bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
1059 lines
36 KiB
Python
1059 lines
36 KiB
Python
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
|
|
import asyncio
|
|
import json
|
|
from functools import partial
|
|
|
|
import pytest
|
|
|
|
from mirage.accessor.s3 import S3Accessor
|
|
from mirage.commands.builtin.generic_bind.adapter import CommandIO
|
|
from mirage.commands.builtin.generic_bind.builders.jq import jq as jq_builder
|
|
from mirage.core.jq import JQ_EMPTY, jq_eval
|
|
from mirage.types import MountMode, PathSpec
|
|
|
|
from .conftest import collect, jq, mem_ws, run_raw, write_to_backend
|
|
|
|
|
|
async def _const_bytes(data, accessor, path, index=None):
|
|
return data
|
|
|
|
|
|
async def _collect_jq(ops, accessor, paths, expr):
|
|
out, _ = await jq_builder(ops, accessor, paths, expr)
|
|
if isinstance(out, bytes):
|
|
return out
|
|
if hasattr(out, "__aiter__"):
|
|
return b"".join([chunk async for chunk in out])
|
|
return b"".join(out)
|
|
|
|
|
|
def test_jq_dot_returns_full_object(backend):
|
|
data = {"a": 1, "b": 2}
|
|
write_to_backend(backend, "/tmp/f.json", json.dumps(data).encode())
|
|
result = jq(backend, "/tmp/f.json", ".")
|
|
assert result == data
|
|
|
|
|
|
def test_jq_key_access(backend):
|
|
write_to_backend(backend, "/tmp/f.json", b'{"name": "alice"}')
|
|
assert jq(backend, "/tmp/f.json", ".name") == "alice"
|
|
|
|
|
|
def test_jq_nested_key(backend):
|
|
write_to_backend(backend, "/tmp/f.json", b'{"a": {"b": 42}}')
|
|
assert jq(backend, "/tmp/f.json", ".a.b") == 42
|
|
|
|
|
|
def test_jq_array_iteration(backend):
|
|
write_to_backend(backend, "/tmp/f.json", b'{"items": [1, 2, 3]}')
|
|
result = jq(backend, "/tmp/f.json", ".items[]")
|
|
assert result == [1, 2, 3]
|
|
|
|
|
|
def test_jq_invalid_expression_raises(backend):
|
|
write_to_backend(backend, "/tmp/f.json", b'{"a": 1}')
|
|
with pytest.raises(ValueError):
|
|
jq(backend, "/tmp/f.json", "a.b")
|
|
|
|
|
|
def test_jq_pipe(backend):
|
|
write_to_backend(backend, "/tmp/f.json", b'{"items": [1, 2, 3]}')
|
|
result = jq(backend, "/tmp/f.json", ".items | length")
|
|
assert result == 3
|
|
|
|
|
|
def test_jq_select(backend):
|
|
write_to_backend(backend, "/tmp/f.json", b'[{"a": 1}, {"a": 2}, {"a": 3}]')
|
|
result = jq(backend, "/tmp/f.json", ".[] | select(.a > 1)")
|
|
assert result == [{"a": 2}, {"a": 3}]
|
|
|
|
|
|
def test_jq_map(backend):
|
|
write_to_backend(backend, "/tmp/f.json", b'[1, 2, 3]')
|
|
result = jq(backend, "/tmp/f.json", "map(. > 1)")
|
|
assert result == [False, True, True]
|
|
|
|
|
|
def test_jq_keys(backend):
|
|
write_to_backend(backend, "/tmp/f.json", b'{"b": 1, "a": 2}')
|
|
result = jq(backend, "/tmp/f.json", "keys")
|
|
assert result == ["a", "b"]
|
|
|
|
|
|
def test_jq_values_passes_through_non_null(backend):
|
|
write_to_backend(backend, "/tmp/f.json", b'{"a": 1, "b": 2}')
|
|
result = jq(backend, "/tmp/f.json", "values")
|
|
assert result == {"a": 1, "b": 2}
|
|
|
|
|
|
def test_jq_values_drops_null(backend):
|
|
write_to_backend(backend, "/tmp/f.json", b'{"a": 1}')
|
|
assert jq(backend, "/tmp/f.json", ".missing | values") is JQ_EMPTY
|
|
|
|
|
|
def test_jq_object_values_via_spread(backend):
|
|
write_to_backend(backend, "/tmp/f.json", b'{"a": 1, "b": 2}')
|
|
result = jq(backend, "/tmp/f.json", "[.[]]")
|
|
assert result == [1, 2]
|
|
|
|
|
|
def test_jq_array_slice(backend):
|
|
write_to_backend(backend, "/tmp/f.json", b'[10, 20, 30, 40, 50]')
|
|
result = jq(backend, "/tmp/f.json", ".[1:3]")
|
|
assert result == [20, 30]
|
|
|
|
|
|
def test_jq_length(backend):
|
|
write_to_backend(backend, "/tmp/f.json", b'[1, 2, 3]')
|
|
result = jq(backend, "/tmp/f.json", "length")
|
|
assert result == 3
|
|
|
|
|
|
def test_jq_string_interpolation():
|
|
result = jq_eval({"name": "world"}, '"Hello \\(.name)"')
|
|
assert result == "Hello world"
|
|
|
|
|
|
def test_jq_string_interpolation_nested():
|
|
result = jq_eval({"a": 1, "b": 2}, '"\\(.a) + \\(.b)"')
|
|
assert result == "1 + 2"
|
|
|
|
|
|
def test_jq_try_catch_returns_null_when_no_error():
|
|
# In real jq, .missing.x is NOT an error; it returns null.
|
|
# Hence catch is never triggered.
|
|
assert jq_eval({}, 'try .missing.x catch "fallback"') is None
|
|
|
|
|
|
def test_jq_try_catch_triggered_on_real_error():
|
|
assert jq_eval([1, 2, 3], 'try .name catch "fallback"') == "fallback"
|
|
|
|
|
|
def test_jq_try_no_catch_returns_null():
|
|
assert jq_eval({}, "try .missing.x") is None
|
|
|
|
|
|
def test_jq_try_no_catch_swallows_real_error():
|
|
assert jq_eval([1, 2, 3], "try .name") is JQ_EMPTY
|
|
|
|
|
|
class TestJqMapValues:
|
|
|
|
def test_map_values_dict(self):
|
|
result = jq_eval({"a": 1, "b": 2}, "map_values(. > 1)")
|
|
assert result == {"a": False, "b": True}
|
|
|
|
def test_map_values_list(self):
|
|
result = jq_eval([1, 2, 3], "map_values(. > 1)")
|
|
assert result == [False, True, True]
|
|
|
|
def test_map_values_dict_arithmetic(self):
|
|
result = jq_eval({"x": 10, "y": 20}, 'map_values(type)')
|
|
assert result == {"x": "number", "y": "number"}
|
|
|
|
|
|
class TestJqHas:
|
|
|
|
def test_has_present_key(self):
|
|
assert jq_eval({"name": "alice"}, 'has("name")') is True
|
|
|
|
def test_has_absent_key(self):
|
|
assert jq_eval({"name": "alice"}, 'has("age")') is False
|
|
|
|
def test_has_array_with_int_key(self):
|
|
# In real jq, has() on arrays takes an integer index.
|
|
assert jq_eval([1, 2, 3], "has(1)") is True
|
|
assert jq_eval([1, 2, 3], "has(5)") is False
|
|
|
|
|
|
class TestJqContains:
|
|
|
|
def test_contains_string_in_string(self):
|
|
assert jq_eval("foobar", 'contains("foo")') is True
|
|
|
|
def test_contains_string_not_found(self):
|
|
assert jq_eval("foobar", 'contains("xyz")') is False
|
|
|
|
def test_contains_element_requires_array_arg(self):
|
|
# In real jq, contains(x) requires arg of same type.
|
|
# For arrays, use contains([item]) — see test_contains_subarray.
|
|
with pytest.raises(ValueError):
|
|
jq_eval([1, 2, 3], "contains(2)")
|
|
|
|
def test_contains_subarray(self):
|
|
assert jq_eval([1, 2, 3], "contains([1, 2])") is True
|
|
|
|
def test_contains_subarray_not_found(self):
|
|
assert jq_eval([1, 2, 3], "contains([4, 5])") is False
|
|
|
|
|
|
class TestJqComparisons:
|
|
|
|
def test_eq_true(self):
|
|
assert jq_eval({"a": 1}, ".a == 1") is True
|
|
|
|
def test_eq_false(self):
|
|
assert jq_eval({"a": 1}, ".a == 2") is False
|
|
|
|
def test_neq_true(self):
|
|
assert jq_eval({"a": 1}, ".a != 2") is True
|
|
|
|
def test_neq_false(self):
|
|
assert jq_eval({"a": 1}, ".a != 1") is False
|
|
|
|
def test_gt(self):
|
|
assert jq_eval({"a": 5}, ".a > 3") is True
|
|
|
|
def test_gt_false(self):
|
|
assert jq_eval({"a": 1}, ".a > 3") is False
|
|
|
|
def test_lt(self):
|
|
assert jq_eval({"a": 1}, ".a < 3") is True
|
|
|
|
def test_lt_false(self):
|
|
assert jq_eval({"a": 5}, ".a < 3") is False
|
|
|
|
def test_gte(self):
|
|
assert jq_eval({"a": 3}, ".a >= 3") is True
|
|
|
|
def test_gte_false(self):
|
|
assert jq_eval({"a": 2}, ".a >= 3") is False
|
|
|
|
def test_lte(self):
|
|
assert jq_eval({"a": 3}, ".a <= 3") is True
|
|
|
|
def test_lte_false(self):
|
|
assert jq_eval({"a": 5}, ".a <= 3") is False
|
|
|
|
def test_string_eq(self):
|
|
assert jq_eval({"s": "hello"}, '.s == "hello"') is True
|
|
|
|
|
|
class TestJqPipes:
|
|
|
|
def test_three_stage_pipe(self, backend):
|
|
write_to_backend(backend, "/tmp/f.json",
|
|
b'{"data": {"items": [1, 2, 3]}}')
|
|
result = jq(backend, "/tmp/f.json", ".data | .items | length")
|
|
assert result == 3
|
|
|
|
def test_pipe_with_iteration_and_select(self, backend):
|
|
write_to_backend(backend, "/tmp/f.json",
|
|
b'[{"x": 1}, {"x": 5}, {"x": 10}]')
|
|
result = jq(backend, "/tmp/f.json", ".[] | select(.x > 3)")
|
|
assert result == [{"x": 5}, {"x": 10}]
|
|
|
|
def test_pipe_keys_then_length(self):
|
|
result = jq_eval({"a": 1, "b": 2, "c": 3}, "keys | length")
|
|
assert result == 3
|
|
|
|
def test_pipe_object_values_via_spread_then_sort(self):
|
|
# Real jq: `values` returns the object as-is (it's the identity for
|
|
# non-null). To get the object's values list, use [.[]].
|
|
result = jq_eval({"a": 3, "b": 1, "c": 2}, "[.[]] | sort")
|
|
assert result == [1, 2, 3]
|
|
|
|
def test_pipe_map_then_select(self):
|
|
data = [1, 2, 3, 4, 5]
|
|
result = jq_eval(data, "map(select(. > 2))")
|
|
assert result == [3, 4, 5]
|
|
|
|
|
|
class TestJqArrayAccess:
|
|
|
|
def test_index_access(self):
|
|
result = jq_eval({"items": ["a", "b", "c"]}, ".items[0]")
|
|
assert result == "a"
|
|
|
|
def test_index_access_last(self):
|
|
result = jq_eval({"items": ["a", "b", "c"]}, ".items[2]")
|
|
assert result == "c"
|
|
|
|
def test_nested_array_iteration(self, backend):
|
|
write_to_backend(backend, "/tmp/f.json",
|
|
b'{"users": [{"name": "alice"}, {"name": "bob"}]}')
|
|
result = jq(backend, "/tmp/f.json", ".users[].name")
|
|
assert result == ["alice", "bob"]
|
|
|
|
def test_slice_from_start(self):
|
|
result = jq_eval([1, 2, 3, 4, 5], ".[:2]")
|
|
assert result == [1, 2]
|
|
|
|
def test_slice_to_end(self):
|
|
result = jq_eval([1, 2, 3, 4, 5], ".[3:]")
|
|
assert result == [4, 5]
|
|
|
|
|
|
class TestJqEdgeCases:
|
|
|
|
def test_empty_object(self):
|
|
assert jq_eval({}, ".") == {}
|
|
|
|
def test_empty_array(self):
|
|
assert jq_eval([], ".") == []
|
|
|
|
def test_deeply_nested(self):
|
|
data = {"a": {"b": {"c": {"d": 42}}}}
|
|
assert jq_eval(data, ".a.b.c.d") == 42
|
|
|
|
def test_unicode_content(self, backend):
|
|
data = {"name": "café", "emoji": "hello"}
|
|
write_to_backend(backend, "/tmp/f.json", json.dumps(data).encode())
|
|
result = jq(backend, "/tmp/f.json", ".name")
|
|
assert result == "café"
|
|
|
|
def test_json_literal_number(self):
|
|
assert jq_eval({}, "42") == 42
|
|
|
|
def test_json_literal_string(self):
|
|
assert jq_eval({}, '"hello"') == "hello"
|
|
|
|
def test_json_literal_array(self):
|
|
assert jq_eval({}, "[1, 2, 3]") == [1, 2, 3]
|
|
|
|
def test_length_of_string(self):
|
|
assert jq_eval("hello", "length") == 5
|
|
|
|
def test_length_of_dict(self):
|
|
assert jq_eval({"a": 1, "b": 2}, "length") == 2
|
|
|
|
def test_keys_of_array(self):
|
|
assert jq_eval(["a", "b", "c"], "keys") == [0, 1, 2]
|
|
|
|
def test_string_interpolation_with_nested_expr(self):
|
|
data = {"items": [1, 2, 3]}
|
|
result = jq_eval(data, '"count: \\(.items | length)"')
|
|
assert result == "count: 3"
|
|
|
|
def test_select_drops_non_matching(self):
|
|
assert jq_eval({"a": 1}, "select(.a > 10)") is JQ_EMPTY
|
|
|
|
def test_map_requires_iterable(self):
|
|
# Real jq: map(.) on a non-iterable raises ValueError.
|
|
with pytest.raises(ValueError):
|
|
jq_eval(5, "map(.)")
|
|
|
|
def test_missing_key_returns_null(self):
|
|
# Real jq: missing keys silently return null (no exception).
|
|
assert jq_eval({"a": 1}, ".b") is None
|
|
|
|
def test_type_error_dot_on_list(self):
|
|
with pytest.raises((TypeError, KeyError, ValueError)):
|
|
jq_eval([1, 2, 3], ".name")
|
|
|
|
def test_try_catch_type_error(self):
|
|
result = jq_eval([1, 2], 'try .name catch "no key"')
|
|
assert result == "no key"
|
|
|
|
|
|
class TestJqComplex:
|
|
|
|
def test_map_select_keys(self):
|
|
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
|
|
result = jq_eval(data, "map(select(.a > 1)) | .[] | .b")
|
|
assert result == [4]
|
|
|
|
def test_iteration_pipe_type(self):
|
|
data = [1, "hello", True, None, [1], {"a": 1}]
|
|
result = jq_eval(data, ".[] | type")
|
|
assert result == [
|
|
"number", "string", "boolean", "null", "array", "object"
|
|
]
|
|
|
|
def test_flatten_then_unique_then_sort(self):
|
|
data = [[3, 1], [2, 1], [3, 2]]
|
|
result = jq_eval(data, "flatten | unique | sort")
|
|
assert result == [1, 2, 3]
|
|
|
|
def test_map_length(self):
|
|
data = ["hello", "hi", "hey"]
|
|
result = jq_eval(data, "map(length)")
|
|
assert result == [5, 2, 3]
|
|
|
|
def test_select_with_has(self):
|
|
data = [{"name": "alice"}, {"age": 30}, {"name": "bob"}]
|
|
result = jq_eval(data, 'map(select(has("name")))')
|
|
assert result == [{"name": "alice"}, {"name": "bob"}]
|
|
|
|
|
|
class TestJqArithmetic:
|
|
|
|
def test_add_numbers(self):
|
|
assert jq_eval({"a": 1, "b": 2}, ".a + .b") == 3
|
|
|
|
def test_subtract(self):
|
|
assert jq_eval({"a": 10, "b": 3}, ".a - .b") == 7
|
|
|
|
def test_multiply(self):
|
|
assert jq_eval({"a": 4, "b": 5}, ".a * .b") == 20
|
|
|
|
def test_divide(self):
|
|
assert jq_eval({"a": 10, "b": 2}, ".a / .b") == 5.0
|
|
|
|
def test_add_string_concat(self):
|
|
assert jq_eval({
|
|
"a": "hello",
|
|
"b": " world"
|
|
}, ".a + .b") == "hello world"
|
|
|
|
def test_add_literal_number(self):
|
|
assert jq_eval({"a": 1}, ".a + 10") == 11
|
|
|
|
|
|
class TestJqObjectConstruction:
|
|
|
|
def test_object_construction(self):
|
|
data = {"name": "alice", "age": 30}
|
|
result = jq_eval(data, "{name: .name}")
|
|
assert result == {"name": "alice"}
|
|
|
|
def test_object_construction_multiple_keys(self):
|
|
data = {"x": 1, "y": 2, "z": 3}
|
|
result = jq_eval(data, "{a: .x, b: .y}")
|
|
assert result == {"a": 1, "b": 2}
|
|
|
|
|
|
class TestJqAlternative:
|
|
|
|
def test_alternative_with_null(self):
|
|
assert jq_eval({"a": None}, '.a // "default"') == "default"
|
|
|
|
def test_alternative_with_value(self):
|
|
assert jq_eval({"a": 42}, '.a // "default"') == 42
|
|
|
|
def test_alternative_missing_key(self):
|
|
assert jq_eval({}, '.missing // "fallback"') == "fallback"
|
|
|
|
|
|
class TestJqIfThenElse:
|
|
|
|
def test_if_then_else_true(self):
|
|
result = jq_eval({"x": 5}, 'if .x > 3 then "big" else "small" end')
|
|
assert result == "big"
|
|
|
|
def test_if_then_else_false(self):
|
|
result = jq_eval({"x": 1}, 'if .x > 3 then "big" else "small" end')
|
|
assert result == "small"
|
|
|
|
|
|
class TestJqSortByGroupBy:
|
|
|
|
def test_sort_by(self):
|
|
data = [{"a": 3}, {"a": 1}, {"a": 2}]
|
|
result = jq_eval(data, "sort_by(.a)")
|
|
assert result == [{"a": 1}, {"a": 2}, {"a": 3}]
|
|
|
|
def test_group_by(self):
|
|
data = [{"k": "a", "v": 1}, {"k": "b", "v": 2}, {"k": "a", "v": 3}]
|
|
result = jq_eval(data, "group_by(.k)")
|
|
assert result == [
|
|
[{
|
|
"k": "a",
|
|
"v": 1
|
|
}, {
|
|
"k": "a",
|
|
"v": 3
|
|
}],
|
|
[{
|
|
"k": "b",
|
|
"v": 2
|
|
}],
|
|
]
|
|
|
|
|
|
class TestJqBugFixes:
|
|
|
|
def test_double_array_iteration(self):
|
|
data = [[1, 2], [3, 4]]
|
|
result = jq_eval(data, ".[] | .[]")
|
|
assert result == [1, 2, 3, 4]
|
|
|
|
def test_double_array_iteration_with_tail(self):
|
|
data = [["ab", "cd"], ["ef"]]
|
|
result = jq_eval(data, ".[] | .[] | length")
|
|
assert result == [2, 2, 2]
|
|
|
|
def test_dot_key_on_list_raises(self):
|
|
# Real jq raises ValueError "Cannot index array with string".
|
|
with pytest.raises(ValueError):
|
|
jq_eval([1, 2, 3], ".name")
|
|
|
|
def test_contains_dict_subset(self):
|
|
assert jq_eval({"a": 1, "b": 2}, 'contains({"a": 1})') is True
|
|
|
|
def test_contains_dict_missing_key(self):
|
|
assert jq_eval({"a": 1}, 'contains({"b": 2})') is False
|
|
|
|
def test_contains_dict_wrong_value(self):
|
|
assert jq_eval({"a": 1}, 'contains({"a": 2})') is False
|
|
|
|
def test_contains_dict_full_match(self):
|
|
assert jq_eval({"a": 1}, 'contains({"a": 1})') is True
|
|
|
|
|
|
class TestJqParensAndArrayConstruction:
|
|
|
|
def test_parens_unwrap_single(self):
|
|
assert jq_eval({"items": [1, 2, 3]}, "(.items | length)") == 3
|
|
|
|
def test_parens_in_object_value(self):
|
|
result = jq_eval({"items": [1, 2, 3]},
|
|
"{n: (.items | length), first: .items[0]}")
|
|
assert result == {"n": 3, "first": 1}
|
|
|
|
def test_array_construction_collects_spread(self):
|
|
data = {"slides": [{"id": "a"}, {"id": "b"}, {"id": "c"}]}
|
|
assert jq_eval(data, "[.slides[].id]") == ["a", "b", "c"]
|
|
|
|
def test_array_construction_wraps_single_value(self):
|
|
assert jq_eval({"x": 5}, "[.x]") == [5]
|
|
|
|
def test_object_with_array_literal_value_does_not_split_inside(self):
|
|
assert jq_eval({}, "{x: 1, y: [1,2,3]}") == {"x": 1, "y": [1, 2, 3]}
|
|
|
|
def test_object_with_nested_object_value(self):
|
|
result = jq_eval({"a": 1, "b": 2}, "{outer: {x: .a, y: .b}}")
|
|
assert result == {"outer": {"x": 1, "y": 2}}
|
|
|
|
def test_join_separator(self):
|
|
assert jq_eval([1, 2, 3], 'join("-")') == "1-2-3"
|
|
|
|
def test_join_empty_separator(self):
|
|
assert jq_eval(["foo", "bar"], 'join("")') == "foobar"
|
|
|
|
def test_array_construction_then_join(self):
|
|
data = {"slides": [{"id": "a"}, {"id": "b"}]}
|
|
assert jq_eval(data, '[.slides[].id] | join(",")') == "a,b"
|
|
|
|
def test_full_slides_summary_expression(self):
|
|
data = {
|
|
"title":
|
|
"Deck",
|
|
"slides": [
|
|
{
|
|
"objectId":
|
|
"s1",
|
|
"pageElements": [
|
|
{
|
|
"shape": {
|
|
"shapeType": "TITLE",
|
|
"text": {
|
|
"textElements": [
|
|
{
|
|
"textRun": {
|
|
"content": "Hello "
|
|
}
|
|
},
|
|
{
|
|
"textRun": {
|
|
"content": "world"
|
|
}
|
|
},
|
|
]
|
|
},
|
|
}
|
|
},
|
|
{
|
|
"image": {
|
|
"url": "..."
|
|
}
|
|
},
|
|
],
|
|
},
|
|
{
|
|
"objectId":
|
|
"s2",
|
|
"pageElements": [{
|
|
"shape": {
|
|
"shapeType": "TEXT_BOX",
|
|
"text": {
|
|
"textElements": [{
|
|
"textRun": {
|
|
"content": "Bye"
|
|
}
|
|
}]
|
|
},
|
|
}
|
|
}],
|
|
},
|
|
],
|
|
}
|
|
expr = ('{title: .title, slideCount: (.slides | length), '
|
|
'slides: [.slides[] | {objectId, '
|
|
'elements: [.pageElements[] | select(.shape != null) | '
|
|
'{type: .shape.shapeType, '
|
|
'text: [.shape.text.textElements[].textRun.content] '
|
|
'| join("")}]}]}')
|
|
assert jq_eval(data, expr) == {
|
|
"title":
|
|
"Deck",
|
|
"slideCount":
|
|
2,
|
|
"slides": [
|
|
{
|
|
"objectId": "s1",
|
|
"elements": [{
|
|
"type": "TITLE",
|
|
"text": "Hello world"
|
|
}],
|
|
},
|
|
{
|
|
"objectId": "s2",
|
|
"elements": [{
|
|
"type": "TEXT_BOX",
|
|
"text": "Bye"
|
|
}],
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
class TestJqRealLibjqExpressions:
|
|
"""Expressions that broke the homegrown parser before the libjq swap.
|
|
Each is a real-world query an agent might run; if any of these fail
|
|
again it means the engine regressed."""
|
|
|
|
def _slides_doc(self) -> dict:
|
|
return {
|
|
"title":
|
|
"Deck",
|
|
"slides": [
|
|
{
|
|
"objectId":
|
|
"s1",
|
|
"pageElements": [
|
|
{
|
|
"shape": {
|
|
"shapeType": "TITLE",
|
|
"text": {
|
|
"textElements": [
|
|
{
|
|
"textRun": {
|
|
"content": "Hello "
|
|
}
|
|
},
|
|
{
|
|
"paragraphMarker": {}
|
|
},
|
|
{
|
|
"textRun": {
|
|
"content": "world"
|
|
}
|
|
},
|
|
]
|
|
},
|
|
}
|
|
},
|
|
],
|
|
},
|
|
{
|
|
"objectId":
|
|
"s2",
|
|
"pageElements": [{
|
|
"shape": {
|
|
"shapeType": "TEXT_BOX",
|
|
"text": {
|
|
"textElements": [{
|
|
"textRun": {
|
|
"content": "Bye"
|
|
}
|
|
}]
|
|
},
|
|
}
|
|
}],
|
|
},
|
|
],
|
|
}
|
|
|
|
def test_user_query_with_select_textRun_then_join(self):
|
|
expr = ('[.slides[].pageElements[].shape.text.textElements[] | '
|
|
'select(.textRun != null) | .textRun.content] | join("")')
|
|
assert jq_eval(self._slides_doc(), expr) == "Hello worldBye"
|
|
|
|
def test_user_query_array_per_slide_then_join(self):
|
|
expr = ('[.slides[] | '
|
|
'[.pageElements[].shape.text.textElements[].textRun.content] '
|
|
'| join("")]')
|
|
# paragraphMarker has no textRun -> jq returns null; join treats null
|
|
# as empty -> "Hello world" for s1, "Bye" for s2.
|
|
assert jq_eval(self._slides_doc(), expr) == ["Hello world", "Bye"]
|
|
|
|
def test_user_query_full_summary_object(self):
|
|
expr = ('{title: .title, '
|
|
'slideCount: (.slides | length), '
|
|
'slides: [.slides[] | {objectId, '
|
|
'elements: [.pageElements[] | select(.shape != null) | '
|
|
'{type: .shape.shapeType, '
|
|
'text: [.shape.text.textElements[].textRun.content] '
|
|
'| join("")}]}]}')
|
|
assert jq_eval(self._slides_doc(), expr) == {
|
|
"title":
|
|
"Deck",
|
|
"slideCount":
|
|
2,
|
|
"slides": [
|
|
{
|
|
"objectId": "s1",
|
|
"elements": [{
|
|
"type": "TITLE",
|
|
"text": "Hello world"
|
|
}],
|
|
},
|
|
{
|
|
"objectId": "s2",
|
|
"elements": [{
|
|
"type": "TEXT_BOX",
|
|
"text": "Bye"
|
|
}],
|
|
},
|
|
],
|
|
}
|
|
|
|
def test_recurse_then_select(self):
|
|
# `recurse` (..) is a real jq builtin the homegrown didn't have.
|
|
data = {"a": 1, "b": {"c": 2, "d": {"e": 3}}}
|
|
result = jq_eval(data, "[.. | numbers] | sort")
|
|
assert result == [1, 2, 3]
|
|
|
|
def test_string_split_join_round_trip(self):
|
|
assert jq_eval("a-b-c", 'split("-")') == ["a", "b", "c"]
|
|
assert jq_eval(["a", "b", "c"], 'join("-")') == "a-b-c"
|
|
|
|
def test_alternative_operator_default(self):
|
|
# `// "default"` falls back when LHS is null/false.
|
|
assert jq_eval({"a": None}, '.a // "fallback"') == "fallback"
|
|
assert jq_eval({"a": "real"}, '.a // "fallback"') == "real"
|
|
|
|
def test_to_entries_from_entries(self):
|
|
result = jq_eval({"a": 1, "b": 2}, "to_entries")
|
|
assert result == [{"key": "a", "value": 1}, {"key": "b", "value": 2}]
|
|
result2 = jq_eval([{"key": "x", "value": 9}], "from_entries")
|
|
assert result2 == {"x": 9}
|
|
|
|
def test_string_endswith_startswith(self):
|
|
assert jq_eval("foobar", 'startswith("foo")') is True
|
|
assert jq_eval("foobar", 'endswith("bar")') is True
|
|
assert jq_eval("foobar", 'startswith("xyz")') is False
|
|
|
|
def test_test_regex(self):
|
|
assert jq_eval("hello world", 'test("w.rld")') is True
|
|
assert jq_eval("hello world", 'test("xyz")') is False
|
|
|
|
def test_walk_transform(self):
|
|
# `walk` is a real jq builtin the homegrown didn't have.
|
|
data = {"a": "FOO", "b": ["BAR", "BAZ"]}
|
|
result = jq_eval(
|
|
data, 'walk(if type == "string" then ascii_downcase else . end)')
|
|
assert result == {"a": "foo", "b": ["bar", "baz"]}
|
|
|
|
def test_nested_object_with_paren_value(self):
|
|
data = {"items": [1, 2, 3]}
|
|
result = jq_eval(
|
|
data, "{count: (.items | length), max: (.items | max), "
|
|
"sum: ([.items[]] | add)}")
|
|
assert result == {"count": 3, "max": 3, "sum": 6}
|
|
|
|
def test_select_chain_inside_array_construction(self):
|
|
data = {
|
|
"users": [
|
|
{
|
|
"name": "alice",
|
|
"active": True
|
|
},
|
|
{
|
|
"name": "bob",
|
|
"active": False
|
|
},
|
|
{
|
|
"name": "carol",
|
|
"active": True
|
|
},
|
|
]
|
|
}
|
|
result = jq_eval(
|
|
data, "[.users[] | select(.active) | .name] | join(\", \")")
|
|
assert result == "alice, carol"
|
|
|
|
def test_top_level_select_returns_empty_sentinel(self):
|
|
# Real jq: select(false) produces 0 outputs.
|
|
# Our adapter returns JQ_EMPTY so callers serialize as empty bytes.
|
|
assert jq_eval({"x": 5}, "select(.x > 100)") is JQ_EMPTY
|
|
|
|
|
|
class TestJqMemoryBackend:
|
|
|
|
def test_single_path(self):
|
|
ws = mem_ws({"/data.json": b'{"key": "value"}'})
|
|
stdout, _ = run_raw(ws, 'jq .key /data/data.json')
|
|
result = json.loads(collect(stdout))
|
|
assert result == "value"
|
|
|
|
def test_multiple_paths(self):
|
|
ws = mem_ws({"/a.json": b'{"x": 1}', "/b.json": b'{"x": 2}'})
|
|
stdout, _ = run_raw(ws, "jq .x /data/a.json /data/b.json")
|
|
raw = collect(stdout).decode()
|
|
assert "1" in raw
|
|
assert "2" in raw
|
|
|
|
def test_stdin_bytes(self):
|
|
ws = mem_ws()
|
|
stdout, _ = run_raw(ws, "jq .name", stdin=b'{"name": "stdin-test"}')
|
|
result = json.loads(collect(stdout))
|
|
assert result == "stdin-test"
|
|
|
|
def test_dot_returns_full(self):
|
|
ws = mem_ws({"/f.json": b'[1, 2, 3]'})
|
|
stdout, _ = run_raw(ws, "jq . /data/f.json")
|
|
result = json.loads(collect(stdout))
|
|
assert result == [1, 2, 3]
|
|
|
|
def test_missing_expression_defaults_dot(self):
|
|
ws = mem_ws({"/f.json": b'{"a": 1}'})
|
|
stdout, io = run_raw(ws, "jq")
|
|
assert io.exit_code == 0
|
|
assert collect(stdout) == b""
|
|
|
|
def test_pipe_in_command(self):
|
|
ws = mem_ws({"/f.json": b'{"items": [1, 2, 3]}'})
|
|
stdout, _ = run_raw(ws, "jq '.items | length' /data/f.json")
|
|
result = json.loads(collect(stdout))
|
|
assert result == 3
|
|
|
|
def test_select_via_command(self):
|
|
ws = mem_ws({"/f.json": b'[{"a": 1}, {"a": 5}]'})
|
|
stdout, _ = run_raw(ws, "jq '.[] | select(.a > 2)' /data/f.json")
|
|
result = json.loads(collect(stdout))
|
|
assert result == {"a": 5}
|
|
|
|
def test_keys_via_command(self):
|
|
ws = mem_ws({"/f.json": b'{"b": 1, "a": 2}'})
|
|
stdout, _ = run_raw(ws, "jq keys /data/f.json")
|
|
result = json.loads(collect(stdout))
|
|
assert result == ["a", "b"]
|
|
|
|
def test_spread_array_iteration(self):
|
|
ws = mem_ws({"/f.json": b'[10, 20, 30]'})
|
|
stdout, _ = run_raw(ws, "jq '.[]' /data/f.json")
|
|
lines = collect(stdout).strip().splitlines()
|
|
result = [json.loads(line) for line in lines]
|
|
assert result == [10, 20, 30]
|
|
|
|
def test_no_spread_dot_on_array(self):
|
|
ws = mem_ws({"/f.json": b'[10, 20, 30]'})
|
|
stdout, _ = run_raw(ws, "jq '.' /data/f.json")
|
|
result = json.loads(collect(stdout))
|
|
assert result == [10, 20, 30]
|
|
|
|
def test_spread_nested_iteration(self):
|
|
ws = mem_ws({"/f.json": b'[{"x": 1}, {"x": 2}]'})
|
|
stdout, _ = run_raw(ws, "jq '.[].x' /data/f.json")
|
|
lines = collect(stdout).strip().splitlines()
|
|
result = [json.loads(line) for line in lines]
|
|
assert result == [1, 2]
|
|
|
|
def test_spread_via_stdin(self):
|
|
ws = mem_ws()
|
|
stdout, _ = run_raw(ws, "jq '.[]'", stdin=b'[1, 2, 3]')
|
|
lines = collect(stdout).strip().splitlines()
|
|
result = [json.loads(line) for line in lines]
|
|
assert result == [1, 2, 3]
|
|
|
|
def test_no_spread_dot_via_stdin(self):
|
|
ws = mem_ws()
|
|
stdout, _ = run_raw(ws, "jq '.'", stdin=b'[1, 2, 3]')
|
|
result = json.loads(collect(stdout))
|
|
assert result == [1, 2, 3]
|
|
|
|
def test_spread_with_raw_flag(self):
|
|
ws = mem_ws({"/f.json": b'["hello", "world"]'})
|
|
stdout, _ = run_raw(ws, "jq -r '.[]' /data/f.json")
|
|
output = collect(stdout).decode()
|
|
assert output == "hello\nworld\n"
|
|
|
|
def test_spread_select_multiple(self):
|
|
ws = mem_ws({"/f.json": b'[{"a": 1}, {"a": 5}, {"a": 10}]'})
|
|
stdout, _ = run_raw(ws, "jq -c '.[] | select(.a > 4)' /data/f.json")
|
|
lines = collect(stdout).strip().splitlines()
|
|
result = [json.loads(line) for line in lines]
|
|
assert result == [{"a": 5}, {"a": 10}]
|
|
|
|
|
|
class TestJqDiskBackend:
|
|
|
|
def _disk_ws(self, tmp_path, files):
|
|
from mirage.resource.disk.disk import DiskResource
|
|
for name, data in files.items():
|
|
fp = tmp_path / name.lstrip("/")
|
|
fp.parent.mkdir(parents=True, exist_ok=True)
|
|
fp.write_bytes(data)
|
|
disk = DiskResource(str(tmp_path))
|
|
from mirage.workspace import Workspace
|
|
return Workspace(
|
|
{"/disk": (disk, MountMode.WRITE)},
|
|
mode=MountMode.WRITE,
|
|
)
|
|
|
|
def test_single_path(self, tmp_path):
|
|
ws = self._disk_ws(tmp_path, {"/f.json": b'{"val": 42}'})
|
|
stdout, _ = run_raw(ws, "jq .val /disk/f.json")
|
|
result = json.loads(collect(stdout))
|
|
assert result == 42
|
|
|
|
def test_stdin_bytes(self, tmp_path):
|
|
ws = self._disk_ws(tmp_path, {})
|
|
stdout, _ = run_raw(ws, "jq .x", stdin=b'{"x": "disk-stdin"}')
|
|
result = json.loads(collect(stdout))
|
|
assert result == "disk-stdin"
|
|
|
|
def test_nested_key(self, tmp_path):
|
|
ws = self._disk_ws(tmp_path, {"/f.json": b'{"a": {"b": 99}}'})
|
|
stdout, _ = run_raw(ws, "jq .a.b /disk/f.json")
|
|
result = json.loads(collect(stdout))
|
|
assert result == 99
|
|
|
|
def test_array_iteration(self, tmp_path):
|
|
ws = self._disk_ws(tmp_path, {"/f.json": b'{"items": [10, 20, 30]}'})
|
|
stdout, _ = run_raw(ws, "jq '.items[]' /disk/f.json")
|
|
lines = collect(stdout).strip().splitlines()
|
|
result = [json.loads(line) for line in lines]
|
|
assert result == [10, 20, 30]
|
|
|
|
def test_pipe_length(self, tmp_path):
|
|
ws = self._disk_ws(tmp_path, {"/f.json": b'[1, 2, 3, 4]'})
|
|
stdout, _ = run_raw(ws, "jq '. | length' /disk/f.json")
|
|
result = json.loads(collect(stdout))
|
|
assert result == 4
|
|
|
|
def test_file_not_found(self, tmp_path):
|
|
ws = self._disk_ws(tmp_path, {})
|
|
_, io = run_raw(ws, "jq . /disk/missing.json")
|
|
assert io.exit_code != 0
|
|
|
|
def test_spread_iteration(self, tmp_path):
|
|
ws = self._disk_ws(tmp_path, {"/f.json": b'[1, 2, 3]'})
|
|
stdout, _ = run_raw(ws, "jq '.[]' /disk/f.json")
|
|
lines = collect(stdout).strip().splitlines()
|
|
result = [json.loads(line) for line in lines]
|
|
assert result == [1, 2, 3]
|
|
|
|
def test_no_spread_dot_array(self, tmp_path):
|
|
ws = self._disk_ws(tmp_path, {"/f.json": b'[1, 2, 3]'})
|
|
stdout, _ = run_raw(ws, "jq '.' /disk/f.json")
|
|
result = json.loads(collect(stdout))
|
|
assert result == [1, 2, 3]
|
|
|
|
def test_spread_raw(self, tmp_path):
|
|
ws = self._disk_ws(tmp_path, {"/f.json": b'["aa", "bb"]'})
|
|
stdout, _ = run_raw(ws, "jq -r '.[]' /disk/f.json")
|
|
output = collect(stdout).decode()
|
|
assert output == "aa\nbb\n"
|
|
|
|
|
|
class TestJqS3Backend:
|
|
|
|
def _s3_ws(self):
|
|
from mirage.resource.s3.s3 import S3Config, S3Resource
|
|
from mirage.workspace import Workspace
|
|
config = S3Config(bucket="test-bucket", region="us-east-1")
|
|
s3 = S3Resource(config)
|
|
return Workspace(
|
|
{"/s3": (s3, MountMode.READ)},
|
|
mode=MountMode.READ,
|
|
)
|
|
|
|
def test_stdin_bytes(self):
|
|
ws = self._s3_ws()
|
|
stdout, _ = run_raw(ws, "jq .key", stdin=b'{"key": "s3-stdin"}')
|
|
result = json.loads(collect(stdout))
|
|
assert result == "s3-stdin"
|
|
|
|
def test_stdin_dot(self):
|
|
ws = self._s3_ws()
|
|
data = {"a": 1, "b": [2, 3]}
|
|
stdout, _ = run_raw(ws, "jq .", stdin=json.dumps(data).encode())
|
|
result = json.loads(collect(stdout))
|
|
assert result == data
|
|
|
|
def test_stdin_length(self):
|
|
ws = self._s3_ws()
|
|
stdout, _ = run_raw(ws, "jq length", stdin=b'[1, 2, 3]')
|
|
result = json.loads(collect(stdout))
|
|
assert result == 3
|
|
|
|
def _run_jq_path(self, data, expr):
|
|
ops = CommandIO(readdir=None,
|
|
read_bytes=partial(_const_bytes, data),
|
|
read_stream=None,
|
|
stat=None,
|
|
is_mounted=lambda a: True,
|
|
local=False)
|
|
path = PathSpec(resource_path="s3/data.json",
|
|
virtual="/s3/data.json",
|
|
directory="/s3",
|
|
resolved=True)
|
|
return asyncio.run(
|
|
_collect_jq(ops, S3Accessor.__new__(S3Accessor), [path], expr))
|
|
|
|
def test_path_with_mock(self):
|
|
out = self._run_jq_path(b'{"name": "from-s3"}', ".name")
|
|
assert json.loads(out) == "from-s3"
|
|
|
|
def test_path_array_iteration_mock(self):
|
|
out = self._run_jq_path(b'[{"id": 1}, {"id": 2}]', ".[].id")
|
|
result = [json.loads(line) for line in out.strip().splitlines()]
|
|
assert result == [1, 2]
|
|
|
|
def test_path_pipe_length_mock(self):
|
|
out = self._run_jq_path(b'{"items": [1, 2, 3]}', ".items | length")
|
|
assert json.loads(out) == 3
|
|
|
|
def test_spread_iteration_mock(self):
|
|
out = self._run_jq_path(b'[10, 20, 30]', ".[]")
|
|
result = [json.loads(line) for line in out.strip().splitlines()]
|
|
assert result == [10, 20, 30]
|
|
|
|
def test_no_spread_dot_mock(self):
|
|
out = self._run_jq_path(b'[10, 20, 30]', ".")
|
|
assert json.loads(out) == [10, 20, 30]
|
|
|
|
def test_spread_stdin(self):
|
|
ws = self._s3_ws()
|
|
stdout, _ = run_raw(ws, "jq '.[]'", stdin=b'[1, 2, 3]')
|
|
lines = collect(stdout).strip().splitlines()
|
|
result = [json.loads(line) for line in lines]
|
|
assert result == [1, 2, 3]
|
|
|
|
def test_no_spread_dot_stdin(self):
|
|
ws = self._s3_ws()
|
|
stdout, _ = run_raw(ws, "jq '.'", stdin=b'[1, 2, 3]')
|
|
result = json.loads(collect(stdout))
|
|
assert result == [1, 2, 3]
|
|
|
|
def test_disk_multiple_paths(self, tmp_path):
|
|
from mirage.resource.disk.disk import DiskResource
|
|
from mirage.workspace import Workspace
|
|
(tmp_path / "a.json").write_bytes(b'{"x": 1}')
|
|
(tmp_path / "b.json").write_bytes(b'{"x": 2}')
|
|
disk = DiskResource(str(tmp_path))
|
|
ws = Workspace(
|
|
{"/disk": (disk, MountMode.WRITE)},
|
|
mode=MountMode.WRITE,
|
|
)
|
|
stdout, _ = run_raw(ws, "jq .x /disk/a.json /disk/b.json")
|
|
raw = collect(stdout).decode()
|
|
assert "1" in raw
|
|
assert "2" in raw
|