diff --git a/test/common.py b/test/common.py index 67dd2cbb5c7..3743f43daa7 100644 --- a/test/common.py +++ b/test/common.py @@ -12,8 +12,13 @@ # otherwise use the software for commercial activities involving the Arduino # software without disclosing the source code of your own applications. To purchase # a commercial license, send an email to license@arduino.cc. +import collections import os +from invoke.context import Context + +Board = collections.namedtuple("Board", "address fqbn package architecture id core") + def running_on_ci(): """ @@ -21,3 +26,28 @@ def running_on_ci(): """ val = os.getenv("APPVEYOR") or os.getenv("DRONE") or os.getenv("GITHUB_WORKFLOW") return val is not None + + +def build_runner(cli_path, env, working_dir): + """ + Provide a wrapper around invoke's `run` API so that every test + will work in its own temporary folder. + + Useful reference: + http://docs.pyinvoke.org/en/1.2/api/runners.html#invoke.runners.Result + + :param cli_path: the path to the ``arduino-cli`` executable file. + :param env: a ``dict`` with the environment variables to use. + :param working_dir: the CWD where the command will be executed. + + :returns a runner function with the mechanic to run an ``arduino-cli`` instance + with a given environment ``env`` in the directory ```working_dir`. + """ + + def _run(cmd_string): + cli_full_line = "{} {}".format(cli_path, cmd_string) + run_context = Context() + with run_context.cd(working_dir): + return run_context.run(cli_full_line, echo=False, hide=True, warn=True, env=env) + + return _run diff --git a/test/conftest.py b/test/conftest.py index a11e46e2fd0..4e3145ee57b 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -12,10 +12,12 @@ # otherwise use the software for commercial activities involving the Arduino # software without disclosing the source code of your own applications. To purchase # a commercial license, send an email to license@arduino.cc. +import json import os import pytest -from invoke.context import Context + +from .common import build_runner, Board @pytest.fixture(scope="function") @@ -50,23 +52,76 @@ def working_dir(tmpdir_factory): @pytest.fixture(scope="function") def run_command(pytestconfig, data_dir, downloads_dir, working_dir): """ - Provide a wrapper around invoke's `run` API so that every test - will work in the same temporary folder. + Run the ``arduino-cli`` command to perform a the real test on the CLI. + """ + cli_path = os.path.join(pytestconfig.rootdir, "..", "arduino-cli") + env = { + "ARDUINO_DATA_DIR": data_dir, + "ARDUINO_DOWNLOADS_DIR": downloads_dir, + "ARDUINO_SKETCHBOOK_DIR": data_dir, + } + + return build_runner(cli_path, env, working_dir) - Useful reference: - http://docs.pyinvoke.org/en/1.2/api/runners.html#invoke.runners.Result + +@pytest.fixture(scope="session") +def _run_session_command(pytestconfig, tmpdir_factory, downloads_dir): + """ + Run the ``arduino-cli`` command to collect general metadata and store it in + a `session` scope for the tests. """ cli_path = os.path.join(pytestconfig.rootdir, "..", "arduino-cli") + data_dir = tmpdir_factory.mktemp("SessionDataDir") env = { "ARDUINO_DATA_DIR": data_dir, "ARDUINO_DOWNLOADS_DIR": downloads_dir, "ARDUINO_SKETCHBOOK_DIR": data_dir, } + # it looks like the pyinvoke library has a few problems in dealing with the path + # object, so to avoid this issue we can convert them to str. + # Take a look at https://github.com/pyinvoke/invoke/issues/454 for more details. + working_dir = str(tmpdir_factory.mktemp("SessionTestWork")) + + return build_runner(cli_path, env, working_dir) + + +@pytest.fixture(scope="session") +def detected_boards(_run_session_command): + """This fixture provides a list of all the boards attached to the host. + + This fixture will parse the JSON output of the ``arduino-cli board list --format json`` + command to extract all the connected boards data. + + :returns a list ``Board`` objects. + """ + + result = _run_session_command("core update-index") + assert result.ok + + result = _run_session_command("board list --format json") + assert result.ok + + detected_boards = [] + + ports = json.loads(result.stdout) + assert isinstance(ports, list) + for port in ports: + boards = port.get('boards', []) + assert isinstance(boards, list) + for board in boards: + fqbn = board.get('FQBN') + package, architecture, _id = fqbn.split(":") + detected_boards.append( + Board( + address=port.get('address'), + fqbn=fqbn, + package=package, + architecture=architecture, + id=_id, + core="{}:{}".format(package, architecture) + ) + ) - def _run(cmd_string): - cli_full_line = "{} {}".format(cli_path, cmd_string) - run_context = Context() - with run_context.cd(working_dir): - return run_context.run(cli_full_line, echo=False, hide=True, warn=True, env=env) + assert len(detected_boards) >= 1, "There are no boards available for testing" - return _run + return detected_boards diff --git a/test/test_compile.py b/test/test_compile.py index 08756867e10..42348037d9a 100644 --- a/test/test_compile.py +++ b/test/test_compile.py @@ -71,15 +71,7 @@ def test_compile_with_simple_sketch(run_command, data_dir): @pytest.mark.skipif(running_on_ci(), reason="VMs have no serial ports") -def test_compile_and_compile_combo(run_command, data_dir): - # Init the environment explicitly - result = run_command("core update-index") - assert result.ok - - # Install required core(s) - result = run_command("core install arduino:avr") - result = run_command("core install arduino:samd") - assert result.ok +def test_compile_and_compile_combo(run_command, detected_boards, data_dir): # Create a test sketch sketch_name = "CompileAndUploadIntegrationTest" @@ -88,49 +80,23 @@ def test_compile_and_compile_combo(run_command, data_dir): assert result.ok assert "Sketch created in: {}".format(sketch_path) in result.stdout - # - # Build a list of detected boards to test, if any. - # - result = run_command("board list --format json") - assert result.ok - - # - # The `board list --format json` returns a JSON that looks like to the following: - # - # [ - # { - # "address": "/dev/cu.usbmodem14201", - # "protocol": "serial", - # "protocol_label": "Serial Port (USB)", - # "boards": [ - # { - # "name": "Arduino NANO 33 IoT", - # "FQBN": "arduino:samd:nano_33_iot" - # } - # ] - # } - # ] - - detected_boards = [] - - ports = json.loads(result.stdout) - assert isinstance(ports, list) - for port in ports: - boards = port.get('boards') - assert isinstance(boards, list) - for board in boards: - detected_boards.append(dict(address=port.get('address'), fqbn=board.get('FQBN'))) - - assert len(detected_boards) >= 1, "There are no boards available for testing" - # Build sketch for each detected board for board in detected_boards: - log_file_name = "{fqbn}-compile.log".format(fqbn=board.get('fqbn').replace(":", "-")) + + # Init the environment explicitly + result = run_command("core update-index") + assert result.ok + + # Install required core(s) + result = run_command("core install {}".format(board.core)) + assert result.ok + + log_file_name = "{fqbn}-compile.log".format(fqbn=board.fqbn.replace(":", "-")) log_file_path = os.path.join(data_dir, log_file_name) command_log_flags = "--log-format json --log-file {} --log-level trace".format(log_file_path) result = run_command("compile -b {fqbn} --upload -p {address} {sketch_path} {log_flags}".format( - fqbn=board.get('fqbn'), - address=board.get('address'), + fqbn=board.fqbn, + address=board.address, sketch_path=sketch_path, log_flags=command_log_flags )) @@ -139,10 +105,10 @@ def test_compile_and_compile_combo(run_command, data_dir): log_json = open(log_file_path, 'r') json_log_lines = log_json.readlines() expected_trace_sequence = [ - "Compile {sketch} for {fqbn} started".format(sketch=sketch_path, fqbn=board.get('fqbn')), - "Compile {sketch} for {fqbn} successful".format(sketch=sketch_name, fqbn=board.get('fqbn')), - "Upload {sketch} on {fqbn} started".format(sketch=sketch_path, fqbn=board.get('fqbn')), - "Upload {sketch} on {fqbn} successful".format(sketch=sketch_name, fqbn=board.get('fqbn')) + "Compile {sketch} for {fqbn} started".format(sketch=sketch_path, fqbn=board.fqbn), + "Compile {sketch} for {fqbn} successful".format(sketch=sketch_name, fqbn=board.fqbn), + "Upload {sketch} on {fqbn} started".format(sketch=sketch_path, fqbn=board.fqbn), + "Upload {sketch} on {fqbn} successful".format(sketch=sketch_name, fqbn=board.fqbn) ] assert is_message_sequence_in_json_log_traces(expected_trace_sequence, json_log_lines)