hb_ao_test_vectors.erl - AO-Core Test Suite
Overview
Purpose: Comprehensive test suite for AO-Core resolution engine
Module: hb_ao_test_vectors
Framework: EUnit
Coverage: Resolution, devices, get/set operations, paths, devices
This module provides extensive test coverage for the hb_ao resolution engine, testing different execution scenarios with various configuration options.
Dependencies
- Testing:
eunit - HyperBEAM:
hb_ao,hb_test_utils - Includes:
include/hb.hrl
Test Configuration
%% Benchmark timing
-define(BENCHMARK_TIME, 0.25). % 250ms per benchmark
-define(BENCHMARK_ITERATIONS, 1_000). % 1000 iterations minimumModule Type
This module has no explicitly exported public functions - it is a pure EUnit test module. The suite_test_/0 and benchmark_test_/0 generator functions are auto-discovered by EUnit. This module IS the test suite for hb_ao resolution engine.
Test Suite
Functional Tests
1. resolve_simple% Basic resolution of a simple message
Msg = #{<<"path">> => <<"/data">>, <<"data">> => <<"value">>},
{ok, Result} = hb_ao:resolve(Msg, Opts).% Resolve message by ID
ID = crypto:strong_rand_bytes(32),
{ok, Msg} = hb_ao:resolve_many([ID], Opts).% Start resolution with specific device
{ok, Result} = hb_ao:resolve_many({as, DeviceID, Msg}, Opts).% Device with initialization parameters
{ok, Result} = hb_ao:resolve_many(
{as, DeviceID, #{<<"params">> => #{...}}},
Opts
).% Load device from Arweave and execute
{ok, Result} = hb_ao:resolve_many({as, TxID, Msg}, Opts).% Use device for specific path
Msg = #{<<"as">> => DeviceID, <<"path">> => <<"/key">>},
{ok, Result} = hb_ao:resolve(Msg, Opts).% Continue resolution with different device
Msg1 = #{<<"device">> => Dev1, <<"data">> => <<"test">>},
Msg2 = #{<<"path">> => <<"/data">>, <<"as">> => Dev2},
{ok, Result} = hb_ao:resolve(Msg1, Msg2, Opts).% Multiple nested device switches
{ok, Result} = hb_ao:resolve_many([
{as, Dev1, Msg1},
{as, Dev2, Msg2},
{as, Dev3, Msg3}
], Opts).% Resolve same key multiple times
{ok, R1} = hb_ao:resolve(Msg, #{<<"path">> => <<"key">>}, Opts),
{ok, R2} = hb_ao:resolve(Msg, #{<<"path">> => <<"key">>}, Opts),
?assertEqual(R1, R2). % Should return cached result% Multi-path resolution
Msg = #{<<"path">> => [<<"/key1">>, <<"/key2">>]},
{ok, Results} = hb_ao:resolve(Msg, Opts).% Nested path resolution
Msg = #{<<"outer">> => #{<<"inner">> => <<"value">>}},
{ok, Result} = hb_ao:resolve(Msg, #{<<"path">> => <<"/outer/inner">>}, Opts),
?assertEqual(<<"value">>, Result).% Direct binary key resolution
{ok, Result} = hb_ao:resolve(Msg, <<"key">>, Opts).% Key normalization
?assertEqual(<<"test">>, hb_ao:normalize_key(test)),
?assertEqual(<<"42">>, hb_ao:normalize_key(42)).% Device function with arguments
Dev = #{
<<"my_key">> => fun(M1, M2, Opts) -> {ok, <<"result">>} end
},
Msg = #{<<"device">> => Dev},
{ok, Result} = hb_ao:resolve(Msg, #{<<"path">> => <<"my_key">>}, Opts).% Device with global handler
Dev = #{
info => fun() ->
#{
handler => fun(Key, M1, M2, Opts) -> {ok, Key} end
}
end
},
Msg = #{<<"device">> => Dev},
{ok, Result} = hb_ao:resolve(Msg, #{<<"path">> => <<"any_key">>}, Opts).% Device with default fallback
Dev = #{
info => fun() ->
#{
default => fun(Key, M1, M2, Opts) -> {ok, <<"default">>} end
}
end
},
{ok, Result} = hb_ao:resolve(#{<<"device">> => Dev}, #{<<"path">> => <<"unknown">>}, Opts).% Simple get operation
Msg = #{<<"key">> => <<"value">>},
Result = hb_ao:get(Msg, <<"key">>, Opts),
?assertEqual(<<"value">>, Result).% Nested get
Msg = #{<<"outer">> => #{<<"inner">> => <<"value">>}},
Result = hb_ao:get(Msg, <<"/outer/inner">>, Opts),
?assertEqual(<<"value">>, Result).% Multi-level nesting
Msg = #{
<<"a">> => #{
<<"b">> => #{
<<"c">> => <<"value">>
}
}
},
Result = hb_ao:get(Msg, <<"/a/b/c">>, Opts),
?assertEqual(<<"value">>, Result).% Simple set operation
Msg = #{},
Updated = hb_ao:set(Msg, <<"key">>, <<"value">>, Opts),
?assertEqual(<<"value">>, hb_ao:get(Updated, <<"key">>, Opts)).% Get with custom device
Dev = #{
<<"data">> => fun(M1, M2, Opts) -> {ok, <<"custom">>} end
},
Msg = #{<<"device">> => Dev, <<"data">> => <<"original">>},
Result = hb_ao:get(Msg, <<"data">>, Opts),
?assertEqual(<<"custom">>, Result).% Get using alternative device
Result = hb_ao:get(
#{<<"data">> => <<"value">>},
<<"data">>,
#{<<"as">> => CustomDevice}
).% Set via device
Dev = #{
set => fun(M1, M2, Opts) -> {ok, M2} end
},
Updated = hb_ao:set(
#{<<"device">> => Dev},
<<"key">>,
<<"value">>,
Opts
).% Set nested value
Msg = #{},
Updated = hb_ao:set(Msg, <<"/a/b/c">>, <<"value">>, Opts),
?assertEqual(<<"value">>, hb_ao:get(Updated, <<"/a/b/c">>, Opts)).% Deep set with custom device
Dev = #{
set => fun(M1, M2, Opts) -> {ok, hb_ao:set(M1, M2, Opts)} end
},
Updated = hb_ao:set(
#{<<"device">> => Dev},
<<"/nested/key">>,
<<"value">>,
Opts
).% Device with exports list
Dev = #{
info => fun() ->
#{exports => [<<"allowed_key">>]}
end,
<<"allowed_key">> => fun(M1, M2, Opts) -> {ok, <<"result">>} end,
<<"blocked_key">> => fun(M1, M2, Opts) -> {ok, <<"blocked">>} end
},
% allowed_key should work
{ok, _} = hb_ao:resolve(#{<<"device">> => Dev}, #{<<"path">> => <<"allowed_key">>}, Opts),
% blocked_key should fail or use default
Result = hb_ao:resolve(#{<<"device">> => Dev}, #{<<"path">> => <<"blocked_key">>}, Opts).% Device with excludes list
Dev = #{
info => fun() ->
#{
exports => [<<"key1">>, <<"key2">>],
excludes => [<<"key1">>]
}
end
},
% key2 should work, key1 should not% Device with non-binary keys
Dev = #{
atom_key => fun(M1, M2, Opts) -> {ok, <<"result">>} end,
123 => fun(M1, M2, Opts) -> {ok, <<"number">>} end
},
{ok, R1} = hb_ao:resolve(#{<<"device">> => Dev}, #{<<"path">> => <<"atom_key">>}, Opts),
{ok, R2} = hb_ao:resolve(#{<<"device">> => Dev}, #{<<"path">> => <<"123">>}, Opts).% List to map transformation
List = [<<"a">>, <<"b">>, <<"c">>],
Map = hb_ao:normalize_keys(List),
?assertEqual(<<"a">>, maps:get(<<"1">>, Map)),
?assertEqual(<<"b">>, maps:get(<<"2">>, Map)),
?assertEqual(<<"c">>, maps:get(<<"3">>, Map)).% Execution step hook
StepLog = [],
Opts = #{
step => fun(Msg1, Msg2, Result, OptsInner) ->
StepLog ++ [{Msg1, Msg2, Result}]
end
},
{ok, _} = hb_ao:resolve(Msg1, Msg2, Opts),
% StepLog should contain execution stepsBenchmark Suite
1. benchmark_simple% Simple resolution benchmark
benchmark_simple_resolution() ->
Msg = #{<<"data">> => <<"value">>},
Req = #{<<"path">> => <<"/data">>},
run_benchmark(fun() ->
hb_ao:resolve(Msg, Req, #{})
end).% Multi-step resolution benchmark
benchmark_multistep_resolution() ->
Messages = [Msg1, Msg2, Msg3, Msg4, Msg5],
run_benchmark(fun() ->
hb_ao:resolve_many(Messages, #{})
end).% Get operation benchmark
benchmark_get_operation() ->
Msg = #{<<"key">> => <<"value">>},
run_benchmark(fun() ->
hb_ao:get(Msg, <<"key">>, #{})
end).% Set operation benchmark
benchmark_set_operation() ->
Msg = #{<<"existing">> => <<"value">>},
run_benchmark(fun() ->
hb_ao:set(Msg, <<"new">>, <<"data">>, #{})
end).Test Options
test_opts() ->
[
#{name => <<"default">>, opts => #{}},
#{name => <<"no_cache">>, opts => #{cache_control => [<<"no-cache">>]}},
#{name => <<"no_store">>, opts => #{cache_control => [<<"no-store">>]}},
#{name => <<"no_hashpath">>, opts => #{hashpath => ignore}},
#{name => <<"spawn_worker">>, opts => #{spawn_worker => true}}
].Common Patterns
%% Run all tests
rebar3 eunit
%% Run specific test
rebar3 eunit --test hb_ao_test_vectors:resolve_simple_test
%% Run benchmarks
rebar3 eunit --test hb_ao_test_vectors:benchmark_test_
%% Run with specific options
Opts = #{cache_control => [<<"no-cache">>]},
hb_ao_test_vectors:resolve_simple_test(Opts).Test Utilities
%% From hb_test_utils
suite_with_opts(TestSuite, TestOpts) ->
% Run each test with each option set
lists:flatmap(
fun(#{name := Name, opts := Opts}) ->
lists:map(
fun({TestName, Desc, TestFun}) ->
{TestName,
Desc ++ " [" ++ binary_to_list(Name) ++ "]",
fun() -> TestFun(Opts) end}
end,
TestSuite
)
end,
TestOpts
).Benchmark Methodology
run_benchmark(Fun) ->
% Warm-up
[Fun() || _ <- lists:seq(1, 100)],
% Measure
Start = erlang:system_time(microsecond),
Count = run_for_time(?BENCHMARK_TIME * 1000000, Fun, 0),
End = erlang:system_time(microsecond),
% Calculate stats
TotalTime = End - Start,
AvgTime = TotalTime / Count,
OpsPerSec = Count / (TotalTime / 1000000),
{Count, AvgTime, OpsPerSec}.
run_for_time(TimeLimit, Fun, Count) when TimeLimit > 0 ->
Start = erlang:system_time(microsecond),
Fun(),
End = erlang:system_time(microsecond),
Elapsed = End - Start,
run_for_time(TimeLimit - Elapsed, Fun, Count + 1);
run_for_time(_, _, Count) ->
Count.References
- EUnit - Erlang unit testing framework
- hb_ao - Resolution engine under test
- hb_test_utils - Test utilities
Notes
- Comprehensive Coverage: Tests all major resolution scenarios
- Multiple Configurations: Each test runs with different option sets
- Benchmarking: Performance tests for critical operations
- Device Testing: Extensive device system validation
- Path Resolution: Tests simple and complex path handling
- Get/Set Operations: Validates CRUD operations
- Normalization: Tests key and message normalization
- Caching: Tests with/without caching enabled
- Error Handling: Validates error cases
- Command Line: Can run individual tests via rebar3