fix测试用例

This commit is contained in:
anlicheng 2026-04-21 15:59:40 +08:00
parent 835aceb060
commit 72030d55d6
4 changed files with 320 additions and 98 deletions

View File

@ -4,8 +4,6 @@
{dets_dir, "/usr/local/code/tmp/dets/"},
{upload_dir, "/usr/local/code/tmp/upload/"},
{tcp_server, [
{port, 18088}
]},

50
docs/todo.md Normal file
View File

@ -0,0 +1,50 @@
# TODO
## 协议层
### 高优先级
- 将 `message.proto` 收敛为 `efka``iot` 共用的一份单一事实来源,避免协议定义漂移和双端重复维护。
### 中优先级
- 增强 protobuf 映射和 Docker JSON 生成相关的自动化契约测试。
## Docker 层
### 高优先级
- 将 `docker_commands` 拆分为更清晰的层次Docker HTTP 客户端、Docker JSON 构造层、以及 efka 本地补丁逻辑。
- 明确 `docker_task_reporter` 的投递语义;当前只有内存缓冲,还缺少持久化能力和队列上限控制。
### 中优先级
- 丰富 `docker_deploy_manager` 的任务跟踪状态,把部署元数据、耗时、阶段、失败原因等信息纳入统一管理,避免后续再回头重构。
- 统一 `docker_container_service` 的返回结构,避免调用方分支处理 `ok | {ok, binary()} | {error, binary()}` 这种混合形式。
- 消除 `docker_commands` 中重复的 Docker HTTP 响应解析逻辑。
### 低优先级
- 对于像 `docker_events.erl` 这样当前未启用的模块,要么清理掉,要么补充说明其保留原因。
## 订阅与通道层
### 高优先级
- 将 `efka_subscription` 当前基于列表扫描的匹配方式替换为更可扩展的索引结构,例如 trie 或基于 ETS 的索引。
### 中优先级
- 继续收敛 `efka_client` 的职责,将传输层状态管理与 request/cast 协议处理进一步拆开。
- 评估并落实 `service_channel``subscribed_topics` 集合的用途;如果只是被动保存状态,则应简化。
### 低优先级
- 如果 `efka_client` 的缓存指标量继续增长,可以为缓存刷出增加批量发送或节流机制。
## 基础设施层
### 低优先级
- 如果上传相关能力已经不再属于当前运行时,移除或补充说明残留的启动逻辑,例如 `ensure_upload_dir/0`
- 评估是否将 `efka_logger` 与 OTP `logger` 统一,避免长期维护两套日志链路。

View File

@ -11,7 +11,6 @@
start(_StartType, _StartArgs) ->
io:setopts([{encoding, unicode}]),
ensure_upload_dir(),
%%
erlang:system_flag(fullsweep_after, 16),
start_http_server(),
@ -41,13 +40,4 @@ start_http_server() ->
],
{ok, Pid} = cowboy:start_clear(ws_listener, TransOpts, #{env => #{dispatch => Dispatcher}}),
logger:debug("[efka_app] websocket server start at: ~p, pid is: ~p", [Port, Pid]).
ensure_upload_dir() ->
{ok, UploadDir} = application:get_env(efka, upload_dir),
case filelib:is_dir(UploadDir) of
true ->
ok;
false ->
ok = file:make_dir(UploadDir)
end.
logger:debug("[efka_app] websocket server start at: ~p, pid is: ~p", [Port, Pid]).

View File

@ -11,101 +11,285 @@
-include("message_pb.hrl").
%% API
-export([test_pull/0, test_commands/0, test_create_container/0]).
-export([
test_all/0,
test_pull/0,
test_check_image_exist/0,
test_check_image_not_exist/0,
test_create_container/0,
test_create_container_without_create_options/0,
test_create_container_patches_options/0,
test_check_container_exist/0,
test_check_container_not_exist/0,
test_is_container_running/0,
test_start_container/0,
test_stop_container/0,
test_stop_container_with_timeout/0,
test_kill_container/0,
test_kill_container_with_signal/0,
test_remove_container/0,
test_remove_container_with_options/0,
test_get_containers/0
]).
-define(TEST_IMAGE, <<"docker.1ms.run/library/busybox:latest">>).
test_all() ->
ok = test_pull(),
ok = test_check_image_exist(),
ok = test_check_image_not_exist(),
ok = test_create_container(),
ok = test_create_container_without_create_options(),
ok = test_create_container_patches_options(),
ok = test_check_container_exist(),
ok = test_check_container_not_exist(),
ok = test_is_container_running(),
ok = test_start_container(),
ok = test_stop_container(),
ok = test_stop_container_with_timeout(),
ok = test_kill_container(),
ok = test_kill_container_with_signal(),
ok = test_remove_container(),
ok = test_remove_container_with_options(),
ok = test_get_containers().
test_pull() ->
Image = <<"docker.1ms.run/library/nginx:latest">>,
docker_commands:pull_image(Image, fun(Msg) -> logger:debug("msg is: ~p", [Msg]) end).
ok = docker_commands:pull_image(?TEST_IMAGE, fun(Msg) -> logger:debug("msg is: ~p", [Msg]) end).
test_commands() ->
Id = <<"redpanda-console">>,
StopRes = docker_commands:stop_container(Id),
logger:debug("stop res: ~p", [StopRes]),
StartRes = docker_commands:start_container(Id),
logger:debug("start res: ~p", [StartRes]).
test_check_image_exist() ->
ok = test_pull(),
true = docker_commands:check_image_exist(?TEST_IMAGE),
ok.
test_check_image_not_exist() ->
false = docker_commands:check_image_exist(<<"docker.1ms.run/library/not-exists-for-efka-tests:latest">>),
ok.
test_create_container() ->
Name = test_container_name(<<"create">>),
ContainerDir = prepare_container_dir(Name),
try
ok = test_pull(),
{ok, ContainerId} = docker_commands:create_container(ContainerDir, minimal_params(Name)),
true = is_binary(ContainerId),
ok
after
cleanup_container(Name)
end.
test_create_container_without_create_options() ->
Name = test_container_name(<<"create-default">>),
ContainerDir = prepare_container_dir(Name),
try
ok = test_pull(),
{ok, ContainerId} = docker_commands:create_container(ContainerDir, #'ContainerDeployParams'{
container_name = Name
}),
true = is_binary(ContainerId),
Inspect = inspect_container_json(Name),
assert_patched_defaults(Name, ContainerDir, Inspect),
ok
after
cleanup_container(Name)
end.
test_create_container_patches_options() ->
Name = test_container_name(<<"create-patch">>),
ContainerDir = prepare_container_dir(Name),
Params = #'ContainerDeployParams'{
container_name = <<"my_nginx_new1">>,
container_name = Name,
create = #'DockerCreateOptions'{
config = #'DockerContainerConfig'{
image = <<"docker.1ms.run/library/nginx:latest">>,
cmd = [
<<"nginx">>,
<<"-g">>,
<<"daemon off;">>
],
entrypoint = [
<<"/docker-entrypoint.sh">>
],
env = [
<<"ENV1=val1">>,
<<"ENV2=val2">>
],
volumes = [
<<"/data">>,
<<"/var/log">>
],
labels = [
{<<"role">>, <<"web">>},
{<<"env">>, <<"prod">>}
],
user = <<"www-data">>,
working_dir = <<"/app">>,
hostname = <<"myhost">>,
exposed_ports = [
#'DockerExposedPort'{container_port = 80, protocol = <<"tcp">>},
#'DockerExposedPort'{container_port = 443, protocol = <<"tcp">>}
],
healthcheck = #'Healthcheck'{
test = [
<<"CMD-SHELL">>,
<<"curl -f http://localhost || exit 1">>
],
interval_ns = 30000000000,
timeout_ns = 10000000000,
retries = 3
}
image = ?TEST_IMAGE,
cmd = [<<"sleep">>, <<"300">>],
env = [<<"EXISTING_ENV=1">>],
volumes = [<<"/data">>]
},
host_config = #'DockerHostConfig'{
binds = [
<<"/host/data:/data">>,
<<"/host/log:/var/log">>
],
network_mode = <<"bridge">>,
restart_policy = #'RestartPolicy'{name = <<"always">>},
privileged = true,
cap_add = [
<<"NET_ADMIN">>
],
cap_drop = [
<<"MKNOD">>
],
devices = [
#'DeviceMapping'{host_path = <<"/dev/snd">>, container_path = <<"/dev/snd">>}
],
memory = 512 * 1024 * 1024,
memory_reservation = 256 * 1024 * 1024,
cpu_shares = 512,
nano_cpus = 1500000000,
ulimits = [
#'Ulimit'{name = <<"nofile">>, soft = 1024, hard = 2048}
],
sysctls = [
{<<"net.ipv4.ip_forward">>, <<"1">>}
],
tmpfs = [
{<<"/tmp">>, <<>>}
],
extra_hosts = [
<<"host1:192.168.0.1">>
]
},
networking_config = #'DockerNetworkingConfig'{
endpoints = [
#'DockerNetworkEndpoint'{name = <<"mynet">>}
]
binds = [<<"/tmp:/tmp">>]
}
}
},
docker_commands:create_container("/usr/local/code/efka/", Params).
try
ok = test_pull(),
{ok, _ContainerId} = docker_commands:create_container(ContainerDir, Params),
Inspect = inspect_container_json(Name),
assert_patched_defaults(Name, ContainerDir, Inspect),
#{<<"Config">> := #{<<"Env">> := Env, <<"Volumes">> := Volumes},
<<"HostConfig">> := #{<<"Binds">> := Binds}} = Inspect,
true = lists:member(<<"EXISTING_ENV=1">>, Env),
true = maps:is_key(<<"/data">>, Volumes),
true = lists:member(<<"/tmp:/tmp">>, Binds),
ok
after
cleanup_container(Name)
end.
test_check_container_exist() ->
Name = test_container_name(<<"exist">>),
with_created_container(Name, fun(_ContainerDir, _ContainerId) ->
true = docker_commands:check_container_exist(Name),
ok
end).
test_check_container_not_exist() ->
false = docker_commands:check_container_exist(test_container_name(<<"missing">>)),
ok.
test_is_container_running() ->
Name = test_container_name(<<"running">>),
with_created_container(Name, fun(_ContainerDir, ContainerId) ->
false = docker_commands:is_container_running(ContainerId),
ok = docker_commands:start_container(Name),
true = docker_commands:is_container_running(ContainerId),
ok = docker_commands:stop_container(Name),
false = docker_commands:is_container_running(ContainerId),
ok
end).
test_start_container() ->
Name = test_container_name(<<"start">>),
with_created_container(Name, fun(_ContainerDir, ContainerId) ->
ok = docker_commands:start_container(Name),
true = docker_commands:is_container_running(ContainerId),
ok
end).
test_stop_container() ->
Name = test_container_name(<<"stop">>),
with_started_container(Name, fun(_ContainerDir, ContainerId) ->
ok = docker_commands:stop_container(Name),
false = docker_commands:is_container_running(ContainerId),
ok
end).
test_stop_container_with_timeout() ->
Name = test_container_name(<<"stop-timeout">>),
with_started_container(Name, fun(_ContainerDir, ContainerId) ->
ok = docker_commands:stop_container(Name, 1),
false = docker_commands:is_container_running(ContainerId),
ok
end).
test_kill_container() ->
Name = test_container_name(<<"kill">>),
with_started_container(Name, fun(_ContainerDir, ContainerId) ->
ok = docker_commands:kill_container(Name),
timer:sleep(200),
false = docker_commands:is_container_running(ContainerId),
ok
end).
test_kill_container_with_signal() ->
Name = test_container_name(<<"kill-signal">>),
with_started_container(Name, fun(_ContainerDir, ContainerId) ->
ok = docker_commands:kill_container(Name, <<"SIGKILL">>),
timer:sleep(200),
false = docker_commands:is_container_running(ContainerId),
ok
end).
test_remove_container() ->
Name = test_container_name(<<"remove">>),
with_created_container(Name, fun(_ContainerDir, _ContainerId) ->
ok = docker_commands:remove_container(Name),
false = docker_commands:check_container_exist(Name),
ok
end, false).
test_remove_container_with_options() ->
Name = test_container_name(<<"remove-opts">>),
with_started_container(Name, fun(_ContainerDir, _ContainerId) ->
ok = docker_commands:remove_container(Name, true, false),
false = docker_commands:check_container_exist(Name),
ok
end, false).
test_get_containers() ->
Name = test_container_name(<<"list">>),
with_created_container(Name, fun(_ContainerDir, ContainerId) ->
{ok, Containers} = docker_commands:get_containers(),
true = is_list(Containers),
true = contains_container(Name, ContainerId, Containers),
ok
end).
with_created_container(Name, Fun) ->
with_created_container(Name, Fun, true).
with_created_container(Name, Fun, Cleanup) when is_binary(Name), is_function(Fun, 2), is_boolean(Cleanup) ->
ContainerDir = prepare_container_dir(Name),
try
ok = test_pull(),
{ok, ContainerId} = docker_commands:create_container(ContainerDir, minimal_params(Name)),
ok = Fun(ContainerDir, ContainerId)
after
case Cleanup of
true ->
cleanup_container(Name);
false ->
ok
end
end.
with_started_container(Name, Fun) ->
with_started_container(Name, Fun, true).
with_started_container(Name, Fun, Cleanup) when is_binary(Name), is_function(Fun, 2), is_boolean(Cleanup) ->
with_created_container(Name, fun(ContainerDir, ContainerId) ->
ok = docker_commands:start_container(Name),
ok = Fun(ContainerDir, ContainerId)
end, Cleanup).
minimal_params(Name) when is_binary(Name) ->
#'ContainerDeployParams'{
container_name = Name,
create = #'DockerCreateOptions'{
config = #'DockerContainerConfig'{
image = ?TEST_IMAGE,
cmd = [<<"sleep">>, <<"300">>]
}
}
}.
prepare_container_dir(Name) when is_binary(Name) ->
Dir = lists:flatten(io_lib:format("/tmp/efka_docker_tests/~ts/", [Name])),
ok = filelib:ensure_dir(Dir ++ "placeholder"),
ok = file:write_file(Dir ++ "service.conf", <<>>, [write]),
Dir.
cleanup_container(Name) when is_binary(Name) ->
_ = docker_commands:remove_container(Name, true, false),
ok.
inspect_container_json(Name) when is_binary(Name) ->
Url = lists:flatten(io_lib:format("/containers/~s/json", [binary_to_list(Name)])),
{ok, 200, _Headers, Resp} = docker_http:request("GET", Url, <<>>, []),
jiffy:decode(Resp, [return_maps]).
assert_patched_defaults(Name, ContainerDir, Inspect)
when is_binary(Name), is_list(ContainerDir), is_map(Inspect) ->
ConfigFile = list_to_binary(docker_helper:get_config_file(ContainerDir)),
ExpectedBind = <<ConfigFile/binary, ":/usr/local/etc/service.conf">>,
#{<<"Config">> := #{<<"Env">> := Env, <<"Volumes">> := Volumes},
<<"HostConfig">> := #{<<"Binds">> := Binds}} = Inspect,
true = lists:member(<<"CONTAINER_NAME=", Name/binary>>, Env),
true = maps:is_key(<<"/usr/local/etc/service.conf">>, Volumes),
true = lists:member(ExpectedBind, Binds),
ok.
contains_container(Name, ContainerId, Containers) when is_binary(Name), is_binary(ContainerId), is_list(Containers) ->
lists:any(fun(Container) -> container_matches(Name, ContainerId, Container) end, Containers).
container_matches(Name, ContainerId, #{<<"Id">> := Id, <<"Names">> := Names}) when is_binary(Id), is_list(Names) ->
lists:member(<<"/", Name/binary>>, Names) orelse has_id_prefix(ContainerId, Id);
container_matches(_Name, _ContainerId, _Container) ->
false.
has_id_prefix(ExpectedId, ActualId) when is_binary(ExpectedId), is_binary(ActualId) ->
PrefixLen = erlang:min(byte_size(ExpectedId), byte_size(ActualId)),
binary:part(ExpectedId, 0, PrefixLen) =:= binary:part(ActualId, 0, PrefixLen).
test_container_name(Prefix) when is_binary(Prefix) ->
Suffix = integer_to_binary(erlang:unique_integer([positive])),
<<"efka-test-", Prefix/binary, "-", Suffix/binary>>.