diff --git a/README.md b/README.md index 8763e38..06cde23 100644 --- a/README.md +++ b/README.md @@ -495,9 +495,9 @@ The following table enumerates the valid `family_id` options: | Key | Description | |-------------|-------------| | `rp2040` | Original Raspberry Pi Pico and Pico-W | -| `rp2035` | Raspberry Pi Pico 2 | +| `rp2350` | Raspberry Pi Pico 2 | | `data` | Raspberry Pi Pico 2 | -| `universal` | Universal format for both `rp2040` and `rp2035` | +| `universal` | Universal format for both `rp2040` and `rp2350` | > Note the convenience of universal uf2 binaries comes with the expense of being twice the size, as both versions are included in the universal uf2. diff --git a/src/atomvm_pico_flash_provider.erl b/src/atomvm_pico_flash_provider.erl index ae26974..aea8520 100644 --- a/src/atomvm_pico_flash_provider.erl +++ b/src/atomvm_pico_flash_provider.erl @@ -119,11 +119,17 @@ env_opts() -> %% @private find_picotool() -> - case os:find_executable("picotool") of - false -> - ""; - Picotool -> - Picotool + %% We use a mock picotool for CI tests to simulate resetting and mounting a device. + case os:getenv("ATOMVM_REBAR3_TEST_MODE") of + "true" -> + os:getenv("ATOMVM_REBAR3_TEST_PICOTOOL"); + _ -> + case os:find_executable("picotool") of + false -> + ""; + Picotool -> + Picotool + end end. %% @private @@ -201,12 +207,19 @@ wait_for_mount(_Mount, 30) -> %% @private get_pico_mount(Mount) -> + Count = + case os:getenv("ATOMVM_REBAR3_TEST_MODE") of + "true" -> + 25; + _ -> + 0 + end, case Mount of "" -> case find_mounted_pico() of not_found -> rebar_api:info("Waiting for an RP2 device to mount...", []), - wait_for_mount(Mount, 0); + wait_for_mount(Mount, Count); {ok, Pico} -> {ok, Pico} end; @@ -219,7 +232,7 @@ get_pico_mount(Mount) -> rebar_api:info("Waiting for the device at path ~s to mount...", [ string:trim(Mount) ]), - wait_for_mount(Mount, 0) + wait_for_mount(Mount, Count) end end. @@ -270,7 +283,12 @@ do_reset(ResetPort, Picotool) -> rebar_api:warn("Disconnecting serial monitor with: `~s' in 5 seconds...", [ DevReset ]), - timer:sleep(5000), + case os:getenv("ATOMVM_REBAR3_TEST_MODE") of + "true" -> + ok; + _ -> + timer:sleep(5000) + end, RebootReturn = os:cmd(DevReset), RebootStatus = string:trim(RebootReturn), case RebootStatus of @@ -297,6 +315,16 @@ get_uf2_appname(ProjectApps) -> %% @private do_flash(ProjectApps, PicoPath, ResetDev, Picotool) -> + TestMode = os:getenv("ATOMVM_REBAR3_TEST_MODE"), + case TestMode of + "true" -> + rebar_api:info( + "Using picotool options:~n --path ~s~n --reset ~s~n --picotool ~s", + [PicoPath, ResetDev, Picotool] + ); + _ -> + ok + end, case needs_reset(ResetDev) of false -> rebar_api:debug("No Pico reset device found matching ~s.", [ResetDev]), @@ -305,7 +333,15 @@ do_flash(ProjectApps, PicoPath, ResetDev, Picotool) -> rebar_api:debug("Pico at ~s needs reset...", [ResetPort]), do_reset(ResetPort, Picotool), rebar_api:info("Waiting for the device at path ~s to settle and mount...", [PicoPath]), - wait_for_mount(PicoPath, 0) + %% Reduce the timeout for tests since we aren't waiting for real hardware + Count = + case TestMode of + "true" -> + 25; + _ -> + 0 + end, + wait_for_mount(PicoPath, Count) end, {ok, Path} = get_pico_mount(PicoPath), TargetUF2 = get_uf2_file(ProjectApps), diff --git a/src/atomvm_uf2create_provider.erl b/src/atomvm_uf2create_provider.erl index 8dc5cf9..f4e88c2 100644 --- a/src/atomvm_uf2create_provider.erl +++ b/src/atomvm_uf2create_provider.erl @@ -79,7 +79,8 @@ do(State) -> StartAddrStr = parse_addr(maps:get(start, Opts)), Image = maps:get(input, Opts, TargetAVM), Uf2Flavor = validate_flavor(maps:get(family_id, Opts)), - ok = uf2tool:uf2create(Output, Uf2Flavor, StartAddrStr, Image), + ok = do_create_uf2(Output, Uf2Flavor, StartAddrStr, Image), + rebar_api:info("UF2 file written to ~s", [Output]), {ok, State} catch C:E:S -> @@ -99,6 +100,19 @@ format_error(Reason) -> %% internal functions %% +%% @private +do_create_uf2(Output, Uf2Flavor, StartAddrStr, Image) -> + case os:getenv("ATOMVM_REBAR3_TEST_MODE") of + "true" -> + rebar_api:info( + "Using uf2create options:~n --output ~s~n --family_id ~p~n --start 0x~.16B~n --input ~s", + [Output, Uf2Flavor, StartAddrStr, Image] + ); + _ -> + ok + end, + uf2tool:uf2create(Output, Uf2Flavor, StartAddrStr, Image). + %% @private get_opts(State) -> {ParsedArgs, _} = rebar_state:command_parsed_args(State), @@ -153,9 +167,9 @@ validate_flavor(Flavor) -> rp2040; "rp2040" -> rp2040; - rp2035 -> + rp2350 -> data; - "rp2035" -> + "rp2350" -> data; data -> data; diff --git a/test/driver/apps/rebar_overrides/rebar.config b/test/driver/apps/rebar_overrides/rebar.config index 1ee6c48..10c594b 100644 --- a/test/driver/apps/rebar_overrides/rebar.config +++ b/test/driver/apps/rebar_overrides/rebar.config @@ -26,5 +26,7 @@ {atomvm_rebar3_plugin, [ {packbeam, [{start, start}]}, {esp32_flash, [{chip, "esp32c3"}]}, - {stm32_flash, [{offset, "0x1234"}]} + {stm32_flash, [{offset, "0x1234"}]}, + {uf2create, [{start, "0x10180800"}, {family_id, "rp2350"}]}, + {pico_flash, [{reset, "/dev/FAKE0"}]} ]}. diff --git a/test/driver/scripts/picotool.sh b/test/driver/scripts/picotool.sh new file mode 100755 index 0000000..1e016f5 --- /dev/null +++ b/test/driver/scripts/picotool.sh @@ -0,0 +1,39 @@ +#!/bin/sh +## +## Copyright (c) Winford (UncleGrumpy) +## All rights reserved. +## +## SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + +set -e + +Args="${@}" + +while [ "${1}" != "" ]; do + case ${1} in + "reboot" ) + shift + if ( [ ${1} = "-f" ] && [ ${2} = "-u" ] ) then + if [ -L $TEST_MYAPP_LOCK ] then + ## Since we are locked make sure mount doesn't exist or contain test artifacts + [-d $TEST_MYAPP_MOUNT] && rm -r $TEST_MYAPP_MOUNT + rm $TEST_MYAPP_LOCK + mkdir $TEST_MYAPP_MOUNT + elif ([ -L $TEST_REBAR_OVERRIDES_LOCK ]) then + [-d $TEST_REBAR_OVERRIDES_MOUNT] && rm -r $TEST_REBAR_OVERRIDES_MOUNT + rm $TEST_REBAR_OVERRIDES_LOCK + mkdir $TEST_REBAR_OVERRIDES_MOUNT + else + echo "No accessible RP-series devices in BOOTSEL mode were found." + exit 1 + fi + echo "The device was asked to reboot into BOOTSEL mode." + fi + break + esac + shift +done + +echo "${Args}" + +exit 0 diff --git a/test/driver/src/pico_flash_tests.erl b/test/driver/src/pico_flash_tests.erl new file mode 100644 index 0000000..9c4b755 --- /dev/null +++ b/test/driver/src/pico_flash_tests.erl @@ -0,0 +1,184 @@ +%% +%% Copyright (c) 2023 +%% 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. +%% +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% +-module(pico_flash_tests). + +-export([run/1]). + +run(Opts) -> + ok = test_flags(Opts), + ok = test_env_overrides(Opts), + ok = test_rebar_overrides(Opts), + ok. + +%% @private +test_flags(Opts) -> + %% test default options, and no device connected (this test wiill fail if a real pico is connected!) + test_flags(Opts, [], [ + {"--path\n", ""}, + {"--reset\n", ""}, + {"Pico not mounted after 30 seconds.", "giving up..."} + ]), + + %% Simulate a device that needs reset + Reset1 = os:getenv("TEST_MYAPP_LOCK"), + %% devices needing reset can only be device files or symlinks + file:make_symlink("/dev/null", Reset1), + Path1 = os:getenv("TEST_MYAPP_MOUNT"), + %% make sure the mount is not present (picotool.sh will create the mount after removing lock dev) + file:del_dir_r("TEST_MYAPP_MOUNT"), + test_flags( + Opts, + [ + {"-r", Reset1}, + {"-p", Path1} + ], + [ + {"--path", Path1}, + {"--reset", Reset1}, + { + "Copying myapp.uf2 to " ++ Path1, + "===> Successfully loaded myapp application to the device." + } + ] + ), + + ok. + +%% @private +test_flags(Opts, Flags, FlagExpectList) -> + AppsDir = maps:get(apps_dir, Opts), + AppDir = test:make_path([AppsDir, "myapp"]), + + Cmd = create_pico_flash_cmd(AppDir, Flags, []), + Output = test:execute_cmd(Cmd, Opts), + test:debug(Output, Opts), + + lists:foreach( + fun({Flag, Value}) -> + test:expect_contains(io_lib:format("~s ~s", [Flag, Value]), Output) + end, + FlagExpectList + ), + + test:tick(). + +%% @private +test_env_overrides(Opts) -> + Reset = os:getenv("TEST_MYAPP_LOCK"), + file:make_symlink("/dev/null", Reset), + test_env_overrides( + Opts, "ATOMVM_REBAR3_PLUGIN_PICO_RESET_DEV", Reset, "--reset" + ), + + Path = os:getenv("TEST_MYAPP_MOUNT"), + %% there is no dev lock, so we must make sure the mount exists and is empty + file:del_dir_r(Path), + file:make_dir(Path), + test_env_overrides(Opts, "ATOMVM_REBAR3_PLUGIN_PICO_MOUNT_PATH", Path, "--path"), + %% cleanup + file:del_dir_r(Path), + + file:make_dir(Path), + test_env_overrides( + Opts, "ATOMVM_REBAR3_PLUGIN_PICOTOOL", string:trim(os:cmd("which echo")), "--picotool" + ), + %% cleanup + file:del_dir_r(Path), + ok. + +%% @private +test_env_overrides(Opts, EnvVar, Value, Flag) -> + AppsDir = maps:get(apps_dir, Opts), + AppDir = test:make_path([AppsDir, "myapp"]), + %% if we are not testing path overrides use the test path since no device is present + Flags = + case Flag of + "--path" -> + []; + _ -> + [{"-p", os:getenv("TEST_MYAPP_MOUNT")}] + end, + Cmd = create_pico_flash_cmd(AppDir, Flags, [{EnvVar, Value}]), + Output = test:execute_cmd(Cmd, Opts), + test:debug(Output, Opts), + + ok = test:expect_contains(io_lib:format("~s ~s", [Flag, Value]), Output), + + test:tick(). + +%% @private +test_rebar_overrides(Opts) -> + %% the rebar_overrides rebar.config specifies reset /dev/FAKE0 + Path = os:getenv("TEST_REBAR_OVERRIDES_MOUNT"), + file:del_dir_r(Path), + file:make_dir(Path), + test_rebar_overrides( + Opts, + [{"-p", Path}], + "ATOMVM_REBAR3_PLUGIN_PICO_RESET_DEV", + "/dev/ttyACM0", + "--reset", + "/dev/FAKE0" + ), + %% cleanup + file:del_dir_r(Path), + + %% Simulate a device needing reset, the mock picotool.sh will create the mount matching the reset device. + Reset = os:getenv("TEST_REBAR_OVERRIDES_LOCK"), + file:make_symlink("/dev/null", Reset), + test_rebar_overrides( + Opts, + [{"-r", Reset}, {"-p", Path}], + "ATOMVM_REBAR3_PLUGIN_PICO_RESET_DEV", + "/dev/tty.usbserial-0001", + "--reset", + Reset + ), + %% cleanup + file:del_dir_r(Path), + + %% Simulte a device already in BOOTSEL mode + file:make_dir(Path), + test_rebar_overrides( + Opts, + [{"-p", Path}], + "ATOMVM_REBAR3_PLUGIN_PICO_MOUNT_PATH", + "/mnt/RP2350", + "--path", + Path + ), + ok. + +%% @private +test_rebar_overrides(Opts, Flags, EnvVar, Value, Flag, ExpectedValue) -> + AppsDir = maps:get(apps_dir, Opts), + AppDir = test:make_path([AppsDir, "rebar_overrides"]), + + Cmd = create_pico_flash_cmd(AppDir, Flags, [{EnvVar, Value}]), + Output = test:execute_cmd(Cmd, Opts), + test:debug(Output, Opts), + + ok = test:expect_contains(io_lib:format("~s ~s", [Flag, ExpectedValue]), Output), + + test:tick(). + +%% @private +create_pico_flash_cmd(AppDir, Opts, Env) -> + test:create_rebar3_cmd(AppDir, pico_flash, Opts, Env). diff --git a/test/driver/src/test.erl b/test/driver/src/test.erl index e6cde06..954a24f 100644 --- a/test/driver/src/test.erl +++ b/test/driver/src/test.erl @@ -55,10 +55,18 @@ run_tests(Opts) -> ok = packbeam_tests:run(Opts), io:put_chars("\n"), + io:put_chars("uf2create_tests: "), + ok = uf2create_tests:run(Opts), + io:put_chars("\n"), + io:put_chars("esp32_flash_tests: "), ok = esp32_flash_tests:run(Opts), io:put_chars("\n"), + io:put_chars("pico_flash_tests: "), + ok = pico_flash_tests:run(Opts), + io:put_chars("\n"), + io:put_chars("stm32_flash_tests: "), ok = stm32_flash_tests:run(Opts), io:put_chars("\n"), diff --git a/test/driver/src/uf2create_tests.erl b/test/driver/src/uf2create_tests.erl new file mode 100644 index 0000000..b7e10ef --- /dev/null +++ b/test/driver/src/uf2create_tests.erl @@ -0,0 +1,145 @@ +%% +%% Copyright (c) 2025 Winford (UncleGrumpy) +%% 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. +%% +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% +-module(uf2create_tests). + +-export([run/1]). + +run(Opts) -> + ok = test_defaults(Opts), + ok = test_flags(Opts), + ok = test_rebar_overrides(Opts), + ok. + +%% @private +test_defaults(Opts) -> + AppsDir = maps:get(apps_dir, Opts), + AppDir = test:make_path([AppsDir, "myapp"]), + UF2Path = test:make_path([AppDir, "_build/default/lib/myapp.uf2"]), + case test:file_exists(UF2Path) of + ok -> + Del = lists:join(" ", ["rm -v", UF2Path]), + os:cmd(Del); + _ -> + ok + end, + + Cmd = create_uf2create_cmd(AppDir, [], []), + Output = test:execute_cmd(Cmd, Opts), + test:debug(Output, Opts), + + ok = test:expect_contains("UF2 file written to", Output), + ok = test:expect_contains("_build/default/lib/myapp.uf2", Output), + ok = test:file_exists(UF2Path), + + test:tick(). + +test_flags(Opts) -> + test_flags(Opts, [], [ + {"--start", "0x10180000"}, + {"--family_id", "universal"}, + {"--output", "_build/default/lib/myapp.uf2"}, + {"--input", "_build/default/lib/myapp.avm"} + ]), + test_flags(Opts, [{"-s", "0x12345"}, {"-f", "rp2040"}], [ + {"--start", "0x12345"}, + {"--family_id", "rp2040"} + ]), + test_flags(Opts, [{"--family_id", "rp2350"}], [ + {"--family_id", "data"} + ]), + ok. + +test_flags(Opts, Flags, FlagExpectList) -> + AppsDir = maps:get(apps_dir, Opts), + AppDir = test:make_path([AppsDir, "myapp"]), + UF2Path = test:make_path([AppDir, "_build/default/lib/myapp.uf2"]), + case test:file_exists(UF2Path) of + ok -> + Del = lists:join(" ", ["rm -v", UF2Path]), + os:cmd(Del); + _ -> + ok + end, + + Cmd = create_uf2create_cmd(AppDir, Flags, []), + Output = test:execute_cmd(Cmd, Opts), + test:debug(Output, Opts), + + lists:foreach( + fun({Flag, Value}) -> + test:expect_contains(io_lib:format("~s ~s", [Flag, Value]), Output) + end, + FlagExpectList + ), + ok = test:expect_contains("UF2 file written to", Output), + ok = test:expect_contains("_build/default/lib/myapp.uf2", Output), + ok = test:file_exists(UF2Path), + + test:tick(). + +%% @private +test_rebar_overrides(Opts) -> + %% the rebar_overrides rebar.config specifies start address "0x10180800" + test_rebar_overrides( + Opts, [], "ATOMVM_PICO_APP_START", "0xDEADBEEF", "--start", "0x10180800" + ), + + test_rebar_overrides( + Opts, [{"-s", "0x123456"}], "ATOMVM_PICO_APP_START", "0xDEADBEEF", "--start", "0x123456" + ), + + %% the rebar_overrides rebar.config specifies family_id "rp2350" which is 'data' + test_rebar_overrides( + Opts, [], "ATOMVM_PICO_UF2_FAMILY", "rp2040", "--family_id", "data" + ), + + test_rebar_overrides( + Opts, [{"-f", "universal"}], "ATOMVM_PICO_UF2_FAMILY", "rp2040", "--family_id", "universal" + ), + + ok. + +%% @private +test_rebar_overrides(Opts, Flags, EnvVar, Value, Flag, ExpectedValue) -> + AppsDir = maps:get(apps_dir, Opts), + AppDir = test:make_path([AppsDir, "rebar_overrides"]), + UF2Path = test:make_path([AppDir, "_build/default/lib/myapp.uf2"]), + case test:file_exists(UF2Path) of + ok -> + Del = lists:join(" ", ["rm -v", UF2Path]), + os:cmd(Del); + _ -> + ok + end, + + Cmd = create_uf2create_cmd(AppDir, Flags, [{EnvVar, Value}]), + Output = test:execute_cmd(Cmd, Opts), + test:debug(Output, Opts), + + ok = test:expect_contains(io_lib:format("~s ~s", [Flag, ExpectedValue]), Output), + ok = test:expect_contains("UF2 file written to", Output), + ok = test:expect_contains("_build/default/lib/myapp.uf2", Output), + ok = test:file_exists(UF2Path), + + test:tick(). + +%% @private +create_uf2create_cmd(AppDir, Opts, Env) -> + test:create_rebar3_cmd(AppDir, uf2create, Opts, Env). diff --git a/test/run.sh b/test/run.sh index 49a5b69..ad5f09c 100755 --- a/test/run.sh +++ b/test/run.sh @@ -24,6 +24,16 @@ unset ATOMVM_REBAR3_PLUGIN_PICO_RESET_DEV unset ATOMVM_REBAR3_PLUGIN_UF2CREATE_START +export ATOMVM_REBAR3_TEST_MODE="true" +export ATOMVM_PICOTOOL="${test_dir}/scripts/picotool.sh" +export TEST_MYAPP_LOCK="${test_dir}/apps/myapp/_build/default/dev0" +export TEST_MYAPP_MOUNT="${test_dir}/apps/myapp/_build/default/rp2040" +export TEST_REBAR_OVERRIDES_LOCK="${test_dir}/apps/rebar_overrides/_build/default/dev0" +export TEST_REBAR_OVERRIDES_MOUNT="${test_dir}/apps/rebar_overrides/_build/default/rp2350" + cd "${test_dir}" rebar3 escriptize ./_build/default/bin/driver -r "$(pwd)" "$@" + +unset ATOMVM_REBAR3_TEST_MODE ATOMVM_PICOTOOL +unset TEST_MYAPP_LOCK TEST_MYAPP_MOUNT TEST_REBAR_OVERRIDES_LOCK TEST_REBAR_OVERRIDES_MOUNT