diff --git a/.gitattributes b/.gitattributes index 2d8496db504..fbab4e69abe 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,10 +14,12 @@ *.dxf binary *.mpy binary *.der binary +*.bin binary # These should also not be modified by git. tests/basics/string_cr_conversion.py -text tests/basics/string_crlf_conversion.py -text +tests/micropython/test_normalize_newlines.py.exp -text ports/stm32/pybcdc.inf_template -text ports/stm32/usbhost/** -text ports/cc3200/hal/aes.c -text diff --git a/.github/actions/setup_esp32/action.yml b/.github/actions/setup_esp32/action.yml new file mode 100644 index 00000000000..42c44cf76ce --- /dev/null +++ b/.github/actions/setup_esp32/action.yml @@ -0,0 +1,47 @@ +name: Setup ESP-IDF for CI +description: Install ESP-IDF +inputs: + idf_ver: + required: true + type: string + ccache_key: + required: true + type: string + +runs: + using: "composite" + + steps: + - id: python_ver + name: Read the Python version + run: echo PYTHON_VER=py$(python --version | cut -d' ' -f2) | tee "${GITHUB_OUTPUT}" + shell: bash + + - name: Cached ESP-IDF install + id: cache_esp_idf + uses: actions/cache@v5 + with: + path: | + ./esp-idf/ + ~/.espressif/ + !~/.espressif/dist/ + ~/.cache/pip/ + # Cache is keyed on both IDF version (from the job) and Python version (from the runner) + key: esp-idf-${{ inputs.idf_ver }}-${{ steps.python_ver.outputs.PYTHON_VER }} + + - name: Install ESP-IDF packages + if: steps.cache_esp_idf.outputs.cache-hit != 'true' + env: + IDF_VER: ${{ inputs.idf_ver }} + run: tools/ci.sh esp32_idf_setup + shell: bash + + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + key: esp32-${{ inputs.idf_ver }}-${{ inputs.ccache_key }} + + - name: Enable CCache for ESP-IDF + run: echo "IDF_CCACHE_ENABLE=1" >> ${GITHUB_ENV} + shell: bash + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e11cebddb37..8be63024039 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,7 +3,6 @@ others can review your Pull Request. Before submitting, please read: - https://github.com/micropython/micropython/blob/master/CODEOFCONDUCT.md https://github.com/micropython/micropython/wiki/ContributorGuidelines Please check any CI failures that appear after your Pull Request is opened. @@ -12,16 +11,15 @@ ### Summary - + solve, or what improvement does it add? Add links if relevant, + especially links to open issues. --> ### Testing - + If you leave this section empty then your Pull Request may be closed. --> ### Trade-offs and Alternatives @@ -31,3 +29,20 @@ Delete this heading if not relevant (i.e. small fixes) --> + +### Generative AI + + + +I did not use generative AI tools when creating this PR. + +I used generative AI tools when creating this PR, but a human has checked the +code and is responsible for the code and the description above. + + diff --git a/.github/workflows/biome.yml b/.github/workflows/biome.yml index 88744f16ca7..0cde10acc91 100644 --- a/.github/workflows/biome.yml +++ b/.github/workflows/biome.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Biome uses: biomejs/setup-biome@v2 with: diff --git a/.github/workflows/code_formatting.yml b/.github/workflows/code_formatting.yml index 9f30f048cfd..95653d941f9 100644 --- a/.github/workflows/code_formatting.yml +++ b/.github/workflows/code_formatting.yml @@ -10,11 +10,11 @@ jobs: code-formatting: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 - name: Install packages - run: source tools/ci.sh && ci_c_code_formatting_setup + run: tools/ci.sh c_code_formatting_setup - name: Run code formatting - run: source tools/ci.sh && ci_c_code_formatting_run + run: tools/ci.sh c_code_formatting_run - name: Check code formatting run: git diff --exit-code diff --git a/.github/workflows/code_size.yml b/.github/workflows/code_size.yml index 163d4455cff..f3238a85d9a 100644 --- a/.github/workflows/code_size.yml +++ b/.github/workflows/code_size.yml @@ -1,7 +1,6 @@ name: Check code size on: - push: pull_request: paths: - '.github/workflows/*.yml' @@ -11,6 +10,7 @@ on: - 'shared/**' - 'lib/**' - 'ports/bare-arm/**' + - 'ports/esp32/**' - 'ports/mimxrt/**' - 'ports/minimal/**' - 'ports/rp2/**' @@ -24,17 +24,30 @@ concurrency: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 100 - name: Install packages - run: source tools/ci.sh && ci_code_size_setup + run: tools/ci.sh code_size_setup + + - name: Find IDF_NEWEST_VER + id: idf_ver + run: | + echo "IDF_VER="$(yq .env.IDF_NEWEST_VER < .github/workflows/ports_esp32.yml) \ + | tee "${GITHUB_OUTPUT}" + + - name: Setup ESP-IDF + uses: ./.github/actions/setup_esp32 + with: + idf_ver: ${{ steps.idf_ver.outputs.IDF_VER }} + ccache_key: code_size + - name: Build - run: source tools/ci.sh && ci_code_size_build + run: tools/ci.sh code_size_build - name: Compute code size difference - run: tools/metrics.py diff ~/size0 ~/size1 | tee diff + run: source tools/ci.sh && ci_code_size_report - name: Save PR number if: github.event_name == 'pull_request' env: @@ -42,7 +55,7 @@ jobs: run: echo $PR_NUMBER > pr_number - name: Upload diff if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: code-size-report path: | diff --git a/.github/workflows/code_size_comment.yml b/.github/workflows/code_size_comment.yml index 521afad709d..2eed0b06b8e 100644 --- a/.github/workflows/code_size_comment.yml +++ b/.github/workflows/code_size_comment.yml @@ -15,7 +15,7 @@ jobs: steps: - name: 'Download artifact' id: download-artifact - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: result-encoding: string script: | @@ -56,7 +56,7 @@ jobs: run: unzip code-size-report.zip - name: Post comment to pull request if: steps.download-artifact.outputs.result == 'ok' - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{secrets.GITHUB_TOKEN}} script: | diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 2d8b4627aac..e3a9c79bd5e 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -6,8 +6,13 @@ jobs: codespell: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # codespell version should be kept in sync with .pre-commit-config.yml - - run: pip install --user codespell==2.2.6 tomli + - run: pip install --user codespell==2.4.1 tomli - run: codespell - + # additionally check for misspelling of "MicroPython" + - run: | + if git grep -n Micropython -- ":(exclude).github/workflows/codespell.yml"; then + echo "Please correct capitalisation of MicroPython on the above lines" + exit 1 + fi diff --git a/.github/workflows/commit_formatting.yml b/.github/workflows/commit_formatting.yml index 3fdcabc4ca7..6abc3612a00 100644 --- a/.github/workflows/commit_formatting.yml +++ b/.github/workflows/commit_formatting.yml @@ -1,6 +1,6 @@ name: Check commit message formatting -on: [push, pull_request] +on: [pull_request] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -10,9 +10,9 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: - fetch-depth: '100' - - uses: actions/setup-python@v5 + fetch-depth: 100 + - uses: actions/setup-python@v6 - name: Check commit message formatting - run: source tools/ci.sh && ci_commit_formatting_run + run: tools/ci.sh commit_formatting_run diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d01a4b50c98..79755b74197 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,6 +5,8 @@ on: pull_request: paths: - docs/** + - py/** + - tests/cpydiff/** concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -15,9 +17,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 - name: Install Python packages run: pip install -r docs/requirements.txt + - name: Build unix port + run: tools/ci.sh unix_build_helper - name: Build docs run: make -C docs/ html diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 6613f106625..4627247fb9f 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -18,8 +18,6 @@ jobs: embedding: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Build - run: make -C examples/embedding -f micropython_embed.mk && make -C examples/embedding - - name: Run - run: ./examples/embedding/embed | grep "hello world" + run: tools/ci.sh embedding_build diff --git a/.github/workflows/mpremote.yml b/.github/workflows/mpremote.yml index ee91b6360b9..ad5dd454905 100644 --- a/.github/workflows/mpremote.yml +++ b/.github/workflows/mpremote.yml @@ -11,18 +11,28 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: # Setting this to zero means fetch all history and tags, # which hatch-vcs can use to discover the version tag. fetch-depth: 0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 - name: Install build tools run: pip install build - name: Build mpremote wheel - run: cd tools/mpremote && python -m build --wheel + run: | + if ! git rev-parse --verify -q v1.23.0 >/dev/null; then + echo "::error::mpremote wheel build requires recent MicroPython version tags in the forked repo." + echo "" + echo "To fix, push tags from upstream:" + echo " git remote add upstream https://github.com/micropython/micropython.git" + echo " git fetch upstream --tags" + echo " git push origin --tags" + exit 1 + fi + cd tools/mpremote && python -m build --wheel - name: Archive mpremote wheel - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: mpremote path: | diff --git a/.github/workflows/mpy_format.yml b/.github/workflows/mpy_format.yml index baa02ce08d5..ab668c1cb5e 100644 --- a/.github/workflows/mpy_format.yml +++ b/.github/workflows/mpy_format.yml @@ -6,6 +6,8 @@ on: paths: - '.github/workflows/*.yml' - 'examples/**' + - 'mpy-cross/**' + - 'py/**' - 'tests/**' - 'tools/**' @@ -15,10 +17,12 @@ concurrency: jobs: test: - runs-on: ubuntu-20.04 # use 20.04 to get python2 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install packages - run: source tools/ci.sh && ci_mpy_format_setup + run: tools/ci.sh mpy_format_setup - name: Test mpy-tool.py - run: source tools/ci.sh && ci_mpy_format_test + run: tools/ci.sh mpy_format_test + - name: Test mpy-cross debug emitter + run: tools/ci.sh mpy_cross_debug_emitter diff --git a/.github/workflows/ports.yml b/.github/workflows/ports.yml index 1f262b0ba4b..5e71d4d076a 100644 --- a/.github/workflows/ports.yml +++ b/.github/workflows/ports.yml @@ -17,6 +17,6 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Build ports download metadata run: mkdir boards && ./tools/autobuild/build-downloads.py . ./boards diff --git a/.github/workflows/ports_alif.yml b/.github/workflows/ports_alif.yml new file mode 100644 index 00000000000..6fb225937a9 --- /dev/null +++ b/.github/workflows/ports_alif.yml @@ -0,0 +1,33 @@ +name: alif port + +on: + push: + pull_request: + paths: + - '.github/workflows/*.yml' + - 'tools/**' + - 'py/**' + - 'extmod/**' + - 'shared/**' + - 'lib/**' + - 'drivers/**' + - 'ports/alif/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build_alif: + strategy: + fail-fast: false + matrix: + ci_func: # names are functions in ci.sh + - alif_ae3_build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Install packages + run: tools/ci.sh alif_setup + - name: Build ci_${{matrix.ci_func }} + run: tools/ci.sh ${{ matrix.ci_func }} diff --git a/.github/workflows/ports_cc3200.yml b/.github/workflows/ports_cc3200.yml index f178a140587..194483ec218 100644 --- a/.github/workflows/ports_cc3200.yml +++ b/.github/workflows/ports_cc3200.yml @@ -21,8 +21,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install packages - run: source tools/ci.sh && ci_cc3200_setup + run: tools/ci.sh cc3200_setup - name: Build - run: source tools/ci.sh && ci_cc3200_build + run: tools/ci.sh cc3200_build diff --git a/.github/workflows/ports_esp32.yml b/.github/workflows/ports_esp32.yml index 45808b659ad..56dce3b69d9 100644 --- a/.github/workflows/ports_esp32.yml +++ b/.github/workflows/ports_esp32.yml @@ -17,41 +17,47 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + # Oldest and newest supported ESP-IDF versions, should match ports/esp32/README.md + IDF_OLDEST_VER: &oldest "v5.3" + IDF_NEWEST_VER: &newest "v5.5.1" + jobs: build_idf: strategy: fail-fast: false matrix: + idf_ver: + - *oldest + - *newest ci_func: # names are functions in ci.sh - esp32_build_cmod_spiram_s2 - esp32_build_s3_c3 - runs-on: ubuntu-20.04 + - esp32_build_c2_c5_c6 + - esp32_build_p4 + exclude: + # Exclude some jobs on the oldest IDF version, to save resources + - idf_ver: *oldest + ci_func: esp32_build_c2_c5_c6 + - idf_ver: *oldest + ci_func: esp32_build_p4 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - id: idf_ver - name: Read the ESP-IDF version - run: source tools/ci.sh && echo "IDF_VER=$IDF_VER" | tee "$GITHUB_OUTPUT" + # Only the newest IDF version will build the ESP-IDF lockfiles correctly, + # so we need to disable MICROPY_MAINTAINER_BUILD on older versions. + - name: Disable extra checks for older ESP-IDF + id: check_newest_ver + if: ${{ matrix.idf_ver != env.IDF_NEWEST_VER }} + run: echo "MICROPY_MAINTAINER_BUILD=0" >> ${GITHUB_ENV} - - name: Cached ESP-IDF install - id: cache_esp_idf - uses: actions/cache@v4 - with: - path: | - ./esp-idf/ - ~/.espressif/ - !~/.espressif/dist/ - ~/.cache/pip/ - key: esp-idf-${{ steps.idf_ver.outputs.IDF_VER }} - - - name: Install ESP-IDF packages - if: steps.cache_esp_idf.outputs.cache-hit != 'true' - run: source tools/ci.sh && ci_esp32_idf_setup - - - name: ccache - uses: hendrikmuhs/ccache-action@v1.2 + - name: Setup ESP-IDF + uses: ./.github/actions/setup_esp32 with: - key: esp32-${{ matrix.ci_func }} + idf_ver: ${{ matrix.idf_ver }} + ccache_key: ${{ matrix.ci_func }} + - - name: Build ci_${{matrix.ci_func }} - run: source tools/ci.sh && ci_${{ matrix.ci_func }} + - name: Build ci_${{matrix.ci_func }} on ESP-IDF ${{ matrix.idf_ver }} + run: tools/ci.sh ${{ matrix.ci_func }} diff --git a/.github/workflows/ports_esp8266.yml b/.github/workflows/ports_esp8266.yml index 5236edf40b9..eb7f59cdc49 100644 --- a/.github/workflows/ports_esp8266.yml +++ b/.github/workflows/ports_esp8266.yml @@ -21,8 +21,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install packages - run: source tools/ci.sh && ci_esp8266_setup && ci_esp8266_path >> $GITHUB_PATH + run: tools/ci.sh esp8266_setup && tools/ci.sh esp8266_path >> $GITHUB_PATH - name: Build - run: source tools/ci.sh && ci_esp8266_build + run: tools/ci.sh esp8266_build diff --git a/.github/workflows/ports_mimxrt.yml b/.github/workflows/ports_mimxrt.yml index 9e782bf63b3..fd80f3f6329 100644 --- a/.github/workflows/ports_mimxrt.yml +++ b/.github/workflows/ports_mimxrt.yml @@ -19,15 +19,15 @@ concurrency: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest defaults: run: working-directory: 'micropython repo' # test build with space in path steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: path: 'micropython repo' - name: Install packages - run: source tools/ci.sh && ci_mimxrt_setup + run: tools/ci.sh mimxrt_setup - name: Build - run: source tools/ci.sh && ci_mimxrt_build + run: tools/ci.sh mimxrt_build diff --git a/.github/workflows/ports_nrf.yml b/.github/workflows/ports_nrf.yml index d9cffb9778c..bec9a5dfb5b 100644 --- a/.github/workflows/ports_nrf.yml +++ b/.github/workflows/ports_nrf.yml @@ -19,10 +19,10 @@ concurrency: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install packages - run: source tools/ci.sh && ci_nrf_setup + run: tools/ci.sh nrf_setup - name: Build - run: source tools/ci.sh && ci_nrf_build + run: tools/ci.sh nrf_build diff --git a/.github/workflows/ports_powerpc.yml b/.github/workflows/ports_powerpc.yml index c41b13e5ddf..a883d026806 100644 --- a/.github/workflows/ports_powerpc.yml +++ b/.github/workflows/ports_powerpc.yml @@ -21,8 +21,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install packages - run: source tools/ci.sh && ci_powerpc_setup + run: tools/ci.sh powerpc_setup - name: Build - run: source tools/ci.sh && ci_powerpc_build + run: tools/ci.sh powerpc_build diff --git a/.github/workflows/ports_qemu.yml b/.github/workflows/ports_qemu.yml index 57192c43936..0ed95dbe5f9 100644 --- a/.github/workflows/ports_qemu.yml +++ b/.github/workflows/ports_qemu.yml @@ -20,13 +20,21 @@ concurrency: jobs: build_and_test_arm: + strategy: + fail-fast: false + matrix: + ci_func: # names are functions in ci.sh + - bigendian + - sabrelite + - thumb_softfp + - thumb_hardfp runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install packages - run: source tools/ci.sh && ci_qemu_setup_arm - - name: Build and run test suite - run: source tools/ci.sh && ci_qemu_build_arm + run: tools/ci.sh qemu_setup_arm + - name: Build and run test suite ci_qemu_build_arm_${{ matrix.ci_func }} + run: tools/ci.sh qemu_build_arm_${{ matrix.ci_func }} - name: Print failures if: failure() run: tests/run-tests.py --print-failures @@ -34,11 +42,23 @@ jobs: build_and_test_rv32: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - name: Install packages + run: tools/ci.sh qemu_setup_rv32 + - name: Build and run test suite + run: tools/ci.sh qemu_build_rv32 + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + build_and_test_rv64: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 - name: Install packages - run: source tools/ci.sh && ci_qemu_setup_rv32 + run: tools/ci.sh qemu_setup_rv64 - name: Build and run test suite - run: source tools/ci.sh && ci_qemu_build_rv32 + run: tools/ci.sh qemu_build_rv64 - name: Print failures if: failure() run: tests/run-tests.py --print-failures diff --git a/.github/workflows/ports_renesas-ra.yml b/.github/workflows/ports_renesas-ra.yml index b1a30c2f117..920691eca70 100644 --- a/.github/workflows/ports_renesas-ra.yml +++ b/.github/workflows/ports_renesas-ra.yml @@ -19,11 +19,11 @@ concurrency: jobs: build_renesas_ra_board: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install packages - run: source tools/ci.sh && ci_renesas_ra_setup + run: tools/ci.sh renesas_ra_setup - name: Build - run: source tools/ci.sh && ci_renesas_ra_board_build + run: tools/ci.sh renesas_ra_board_build diff --git a/.github/workflows/ports_rp2.yml b/.github/workflows/ports_rp2.yml index 748f38e1438..ea19e2da7ff 100644 --- a/.github/workflows/ports_rp2.yml +++ b/.github/workflows/ports_rp2.yml @@ -24,10 +24,10 @@ jobs: run: working-directory: 'micropython repo' # test build with space in path steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: path: 'micropython repo' - name: Install packages - run: source tools/ci.sh && ci_rp2_setup + run: tools/ci.sh rp2_setup - name: Build - run: source tools/ci.sh && ci_rp2_build + run: tools/ci.sh rp2_build diff --git a/.github/workflows/ports_samd.yml b/.github/workflows/ports_samd.yml index 5bf1826cd17..eb806ceb044 100644 --- a/.github/workflows/ports_samd.yml +++ b/.github/workflows/ports_samd.yml @@ -21,8 +21,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install packages - run: source tools/ci.sh && ci_samd_setup + run: tools/ci.sh samd_setup - name: Build - run: source tools/ci.sh && ci_samd_build + run: tools/ci.sh samd_build diff --git a/.github/workflows/ports_stm32.yml b/.github/workflows/ports_stm32.yml index f5e01dc1f62..2ed730eb4e8 100644 --- a/.github/workflows/ports_stm32.yml +++ b/.github/workflows/ports_stm32.yml @@ -26,11 +26,11 @@ jobs: - stm32_pyb_build - stm32_nucleo_build - stm32_misc_build - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install packages - run: source tools/ci.sh && ci_stm32_setup + run: tools/ci.sh stm32_setup && tools/ci.sh stm32_path >> $GITHUB_PATH - name: Build ci_${{matrix.ci_func }} - run: source tools/ci.sh && ci_${{ matrix.ci_func }} + run: tools/ci.sh ${{ matrix.ci_func }} diff --git a/.github/workflows/ports_unix.yml b/.github/workflows/ports_unix.yml index 1707fdc9fb3..57f4a964b2f 100644 --- a/.github/workflows/ports_unix.yml +++ b/.github/workflows/ports_unix.yml @@ -23,11 +23,11 @@ jobs: minimal: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Build - run: source tools/ci.sh && ci_unix_minimal_build + run: tools/ci.sh unix_minimal_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_minimal_run_tests + run: tools/ci.sh unix_minimal_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures @@ -35,9 +35,9 @@ jobs: reproducible: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Build with reproducible date - run: source tools/ci.sh && ci_unix_minimal_build + run: tools/ci.sh unix_minimal_build env: SOURCE_DATE_EPOCH: 1234567890 - name: Check reproducible build date @@ -46,11 +46,11 @@ jobs: standard: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Build - run: source tools/ci.sh && ci_unix_standard_build + run: tools/ci.sh unix_standard_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_standard_run_tests + run: tools/ci.sh unix_standard_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures @@ -58,11 +58,11 @@ jobs: standard_v2: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Build - run: source tools/ci.sh && ci_unix_standard_v2_build + run: tools/ci.sh unix_standard_v2_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_standard_v2_run_tests + run: tools/ci.sh unix_standard_v2_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures @@ -70,61 +70,99 @@ jobs: coverage: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' - name: Install packages - run: source tools/ci.sh && ci_unix_coverage_setup + run: tools/ci.sh unix_coverage_setup - name: Build - run: source tools/ci.sh && ci_unix_coverage_build + run: tools/ci.sh unix_coverage_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_coverage_run_tests + run: tools/ci.sh unix_coverage_run_tests - name: Test merging .mpy files - run: source tools/ci.sh && ci_unix_coverage_run_mpy_merge_tests + run: tools/ci.sh unix_coverage_run_mpy_merge_tests - name: Build native mpy modules - run: source tools/ci.sh && ci_native_mpy_modules_build + run: tools/ci.sh native_mpy_modules_build - name: Test importing .mpy generated by mpy_ld.py - run: source tools/ci.sh && ci_unix_coverage_run_native_mpy_tests + run: tools/ci.sh unix_coverage_run_native_mpy_tests - name: Run gcov coverage analysis run: | (cd ports/unix && gcov -o build-coverage/py ../../py/*.c || true) (cd ports/unix && gcov -o build-coverage/extmod ../../extmod/*.c || true) - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v6 with: - fail_ci_if_error: true + # Only fail the job on error if a token is set, or we're running against upstream repo. + # This avoids the annoying situation of the job failing on every push to a fork (if no token is set). + fail_ci_if_error: ${{ secrets.CODECOV_TOKEN != '' || github.repository_owner == 'micropython' }} verbose: true + # note: when a fork opens a PR into MicroPython repo, the pull_request trigger can't access + # secrets so this token value will be empty (codecov will do a 'tokenless' upload). token: ${{ secrets.CODECOV_TOKEN }} - name: Print failures if: failure() run: tests/run-tests.py --print-failures coverage_32bit: - runs-on: ubuntu-20.04 # use 20.04 to get libffi-dev:i386 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' - name: Install packages - run: source tools/ci.sh && ci_unix_32bit_setup + run: tools/ci.sh unix_32bit_setup - name: Build - run: source tools/ci.sh && ci_unix_coverage_32bit_build + run: tools/ci.sh unix_coverage_32bit_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_coverage_32bit_run_tests + run: tools/ci.sh unix_coverage_32bit_run_tests - name: Build native mpy modules - run: source tools/ci.sh && ci_native_mpy_modules_32bit_build + run: tools/ci.sh native_mpy_modules_32bit_build - name: Test importing .mpy generated by mpy_ld.py - run: source tools/ci.sh && ci_unix_coverage_32bit_run_native_mpy_tests + run: tools/ci.sh unix_coverage_32bit_run_native_mpy_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures nanbox: - runs-on: ubuntu-20.04 # use 20.04 to get python2, and libffi-dev:i386 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' + - name: Install packages + run: tools/ci.sh unix_32bit_setup + - name: Build + run: tools/ci.sh unix_nanbox_build + - name: Run main test suite + run: tools/ci.sh unix_nanbox_run_tests + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + longlong: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' - name: Install packages - run: source tools/ci.sh && ci_unix_32bit_setup + run: tools/ci.sh unix_32bit_setup - name: Build - run: source tools/ci.sh && ci_unix_nanbox_build + run: tools/ci.sh unix_longlong_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_nanbox_run_tests + run: tools/ci.sh unix_longlong_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures @@ -132,78 +170,102 @@ jobs: float: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Build - run: source tools/ci.sh && ci_unix_float_build + run: tools/ci.sh unix_float_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_float_run_tests + run: tools/ci.sh unix_float_run_tests + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + gil_enabled: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Build + run: tools/ci.sh unix_gil_enabled_build + - name: Run main test suite + run: tools/ci.sh unix_gil_enabled_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures stackless_clang: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install packages - run: source tools/ci.sh && ci_unix_clang_setup + run: tools/ci.sh unix_clang_setup - name: Build - run: source tools/ci.sh && ci_unix_stackless_clang_build + run: tools/ci.sh unix_stackless_clang_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_stackless_clang_run_tests + run: tools/ci.sh unix_stackless_clang_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures float_clang: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install packages - run: source tools/ci.sh && ci_unix_clang_setup + run: tools/ci.sh unix_clang_setup - name: Build - run: source tools/ci.sh && ci_unix_float_clang_build + run: tools/ci.sh unix_float_clang_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_float_clang_run_tests + run: tools/ci.sh unix_float_clang_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures - settrace: + settrace_stackless: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' - name: Build - run: source tools/ci.sh && ci_unix_settrace_build + run: tools/ci.sh unix_settrace_stackless_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_settrace_run_tests + run: tools/ci.sh unix_settrace_stackless_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures - settrace_stackless: + repr_b: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' + - name: Install packages + run: tools/ci.sh unix_32bit_setup - name: Build - run: source tools/ci.sh && ci_unix_settrace_stackless_build + run: tools/ci.sh unix_repr_b_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_settrace_stackless_run_tests + run: tools/ci.sh unix_repr_b_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures macos: - runs-on: macos-latest + runs-on: macos-26 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: '3.8' - name: Build - run: source tools/ci.sh && ci_unix_macos_build + run: tools/ci.sh unix_macos_build - name: Run tests - run: source tools/ci.sh && ci_unix_macos_run_tests + run: tools/ci.sh unix_macos_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures @@ -211,13 +273,18 @@ jobs: qemu_mips: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' - name: Install packages - run: source tools/ci.sh && ci_unix_qemu_mips_setup + run: tools/ci.sh unix_qemu_mips_setup - name: Build - run: source tools/ci.sh && ci_unix_qemu_mips_build + run: tools/ci.sh unix_qemu_mips_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_qemu_mips_run_tests + run: tools/ci.sh unix_qemu_mips_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures @@ -225,13 +292,18 @@ jobs: qemu_arm: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' - name: Install packages - run: source tools/ci.sh && ci_unix_qemu_arm_setup + run: tools/ci.sh unix_qemu_arm_setup - name: Build - run: source tools/ci.sh && ci_unix_qemu_arm_build + run: tools/ci.sh unix_qemu_arm_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_qemu_arm_run_tests + run: tools/ci.sh unix_qemu_arm_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures @@ -239,13 +311,68 @@ jobs: qemu_riscv64: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' - name: Install packages - run: source tools/ci.sh && ci_unix_qemu_riscv64_setup + run: tools/ci.sh unix_qemu_riscv64_setup - name: Build - run: source tools/ci.sh && ci_unix_qemu_riscv64_build + run: tools/ci.sh unix_qemu_riscv64_build - name: Run main test suite - run: source tools/ci.sh && ci_unix_qemu_riscv64_run_tests + run: tools/ci.sh unix_qemu_riscv64_run_tests + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + sanitize_address: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' + - name: Install packages + run: tools/ci.sh unix_coverage_setup + - name: Build + run: tools/ci.sh unix_sanitize_address_build + - name: Run main test suite + run: tools/ci.sh unix_sanitize_address_run_tests + - name: Test merging .mpy files + run: tools/ci.sh unix_coverage_run_mpy_merge_tests + - name: Build native mpy modules + run: tools/ci.sh native_mpy_modules_build + - name: Test importing .mpy generated by mpy_ld.py + run: tools/ci.sh unix_coverage_run_native_mpy_tests + - name: Print failures + if: failure() + run: tests/run-tests.py --print-failures + + sanitize_undefined: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. + # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. + with: + python-version: '3.11' + - name: Install packages + run: tools/ci.sh unix_coverage_setup + - name: Build + run: tools/ci.sh unix_sanitize_undefined_build + - name: Run main test suite + run: tools/ci.sh unix_sanitize_undefined_run_tests + - name: Test merging .mpy files + run: tools/ci.sh unix_coverage_run_mpy_merge_tests + - name: Build native mpy modules + run: tools/ci.sh native_mpy_modules_build + - name: Test importing .mpy generated by mpy_ld.py + run: tools/ci.sh unix_coverage_run_native_mpy_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures diff --git a/.github/workflows/ports_webassembly.yml b/.github/workflows/ports_webassembly.yml index 880f15ab344..f6619cc8976 100644 --- a/.github/workflows/ports_webassembly.yml +++ b/.github/workflows/ports_webassembly.yml @@ -11,6 +11,7 @@ on: - 'shared/**' - 'lib/**' - 'ports/webassembly/**' + - 'tests/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -20,13 +21,13 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install packages - run: source tools/ci.sh && ci_webassembly_setup + run: tools/ci.sh webassembly_setup - name: Build - run: source tools/ci.sh && ci_webassembly_build + run: tools/ci.sh webassembly_build - name: Run tests - run: source tools/ci.sh && ci_webassembly_run_tests + run: tools/ci.sh webassembly_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures diff --git a/.github/workflows/ports_windows.yml b/.github/workflows/ports_windows.yml index 91a3192a10a..1243366d413 100644 --- a/.github/workflows/ports_windows.yml +++ b/.github/workflows/ports_windows.yml @@ -25,51 +25,47 @@ jobs: platform: [x86, x64] configuration: [Debug, Release] variant: [dev, standard] - visualstudio: ['2017', '2019', '2022'] + visualstudio: ['2019', '2022'] include: - - visualstudio: '2017' - runner: windows-latest - vs_version: '[15, 16)' - visualstudio: '2019' - runner: windows-2019 - vs_version: '[16, 17)' + # The v142 toolset (VS 2019 compiler) is pre-installed on + # windows-2022 as a component of VS 2022. Use VS 2022's + # MSBuild and select the v142 toolset via PlatformToolset. + vs_version: '[17, 18)' + platform_toolset: v142 - visualstudio: '2022' - runner: windows-2022 vs_version: '[17, 18)' + platform_toolset: v143 # trim down the number of jobs in the matrix exclude: - variant: standard configuration: Debug - visualstudio: '2019' configuration: Debug + runs-on: windows-2022 env: CI_BUILD_CONFIGURATION: ${{ matrix.configuration }} - runs-on: ${{ matrix.runner }} steps: - - name: Install Visual Studio 2017 - if: matrix.visualstudio == '2017' - run: | - choco install visualstudio2017buildtools - choco install visualstudio2017-workload-vctools - choco install windows-sdk-8.1 - - uses: microsoft/setup-msbuild@v2 + - name: Install Python 3.11 + # As of 20260112 the default Python version in Windows image is 3.12, which breaks settrace tests + # Use 3.11 for now + uses: actions/setup-python@v6 with: - vs-version: ${{ matrix.vs_version }} - - uses: actions/setup-python@v5 - if: matrix.runner == 'windows-2019' + python-version: '3.11' + - uses: microsoft/setup-msbuild@v3 with: - python-version: '3.9' - - uses: actions/checkout@v4 + vs-version: ${{ matrix.vs_version }} + - uses: actions/checkout@v6 - name: Build mpy-cross.exe - run: msbuild mpy-cross\mpy-cross.vcxproj -maxcpucount -property:Configuration=${{ matrix.configuration }} -property:Platform=${{ matrix.platform }} + run: msbuild mpy-cross\mpy-cross.vcxproj -maxcpucount -property:Configuration=${{ matrix.configuration }} -property:Platform=${{ matrix.platform }} -property:PlatformToolset=${{ matrix.platform_toolset }} - name: Update submodules run: git submodule update --init lib/micropython-lib - name: Build micropython.exe - run: msbuild ports\windows\micropython.vcxproj -maxcpucount -property:Configuration=${{ matrix.configuration }} -property:Platform=${{ matrix.platform }} -property:PyVariant=${{ matrix.variant }} + run: msbuild ports\windows\micropython.vcxproj -maxcpucount -property:Configuration=${{ matrix.configuration }} -property:Platform=${{ matrix.platform }} -property:PyVariant=${{ matrix.variant }} -property:PlatformToolset=${{ matrix.platform_toolset }} - name: Get micropython.exe path id: get_path run: | - $exePath="$(msbuild ports\windows\micropython.vcxproj -nologo -v:m -t:ShowTargetPath -property:Configuration=${{ matrix.configuration }} -property:Platform=${{ matrix.platform }} -property:PyVariant=${{ matrix.variant }})" + $exePath="$(msbuild ports\windows\micropython.vcxproj -nologo -v:m -t:ShowTargetPath -property:Configuration=${{ matrix.configuration }} -property:Platform=${{ matrix.platform }} -property:PyVariant=${{ matrix.variant }} -property:PlatformToolset=${{ matrix.platform_toolset }})" echo ("micropython=" + $exePath.Trim()) >> $env:GITHUB_OUTPUT - name: Run tests id: test @@ -103,13 +99,18 @@ jobs: env: i686 - sys: mingw64 env: x86_64 - runs-on: windows-2022 + runs-on: windows-latest env: CHERE_INVOKING: enabled_from_arguments defaults: run: shell: msys2 {0} steps: + - uses: actions/setup-python@v6 + # note: can go back to installing mingw-w64-${{ matrix.env }}-python after + # MSYS2 updates to Python >3.12 (due to settrace compatibility issue) + with: + python-version: '3.11' - uses: msys2/setup-msys2@v2 with: msystem: ${{ matrix.sys }} @@ -118,10 +119,10 @@ jobs: make mingw-w64-${{ matrix.env }}-gcc pkg-config - mingw-w64-${{ matrix.env }}-python3 git diffutils - - uses: actions/checkout@v4 + path-type: inherit # Remove when setup-python is removed + - uses: actions/checkout@v6 - name: Build mpy-cross.exe run: make -C mpy-cross -j2 - name: Update submodules @@ -139,8 +140,8 @@ jobs: cross-build-on-linux: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install packages - run: source tools/ci.sh && ci_windows_setup + run: tools/ci.sh windows_setup - name: Build - run: source tools/ci.sh && ci_windows_build + run: tools/ci.sh windows_build diff --git a/.github/workflows/ports_zephyr.yml b/.github/workflows/ports_zephyr.yml index 444b0973daf..330121d1de6 100644 --- a/.github/workflows/ports_zephyr.yml +++ b/.github/workflows/ports_zephyr.yml @@ -11,6 +11,7 @@ on: - 'shared/**' - 'lib/**' - 'ports/zephyr/**' + - 'tests/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -23,21 +24,41 @@ jobs: - uses: jlumbroso/free-disk-space@main with: # Only free up a few things so this step runs quickly. + # (android would save 9.6GiB, but takes about 13m) + # (large-packages would save 4.6GiB, but takes about 3m) android: false dotnet: true haskell: true large-packages: false docker-images: false + tool-cache: true swap-storage: false - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - id: versions + name: Read Zephyr version + run: source tools/ci.sh && echo "ZEPHYR=$ZEPHYR_VERSION" | tee "$GITHUB_OUTPUT" + - name: Cached Zephyr Workspace + id: cache_workspace + uses: actions/cache@v5 + with: + # note that the Zephyr CI docker image is 15GB. At time of writing + # GitHub caches are limited to 10GB total for a project. So we only + # cache the "workspace" + path: ./zephyrproject + key: zephyr-workspace-${{ steps.versions.outputs.ZEPHYR }} + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + key: zephyr - name: Install packages - run: source tools/ci.sh && ci_zephyr_setup + run: tools/ci.sh zephyr_setup - name: Install Zephyr - run: source tools/ci.sh && ci_zephyr_install + if: steps.cache_workspace.outputs.cache-hit != 'true' + run: tools/ci.sh zephyr_install - name: Build - run: source tools/ci.sh && ci_zephyr_build + run: tools/ci.sh zephyr_build - name: Run main test suite - run: source tools/ci.sh && ci_zephyr_run_tests + run: tools/ci.sh zephyr_run_tests - name: Print failures if: failure() run: tests/run-tests.py --print-failures diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 30ae27c0163..d32ea6d07e5 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -6,10 +6,10 @@ jobs: ruff: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Initialize lv_bindings submodule run: git submodule update --init --recursive user_modules/lv_binding_micropython - # ruff version should be kept in sync with .pre-commit-config.yaml - - run: pip install --user ruff==0.1.3 + # ruff version should be kept in sync with .pre-commit-config.yaml & also micropython-lib + - run: pipx install ruff==0.11.6 - run: ruff check --output-format=github . - run: ruff format --diff . diff --git a/.gitignore b/.gitignore index b9a3e59ed66..b57f42c242f 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,11 @@ TAGS ports/javascript/node_modules .vscode/ + +# Created by ci.sh zephyr targets +/.ccache +/zephyrproject + +# Created by ci.sh esp8266 targets +/xtensa-lx106-elf-standalone.tar.gz +/xtensa-lx106-elf/ diff --git a/.gitmodules b/.gitmodules index 3402338cb36..7d2e200cf81 100644 --- a/.gitmodules +++ b/.gitmodules @@ -35,7 +35,7 @@ url = https://github.com/bluekitchen/btstack.git [submodule "lib/nxp_driver"] path = lib/nxp_driver - url = https://github.com/hathach/nxp_driver.git + url = https://github.com/micropython/nxp_driver.git [submodule "lib/libhydrogen"] path = lib/libhydrogen url = https://github.com/jedisct1/libhydrogen.git @@ -68,6 +68,12 @@ [submodule "lib/arduino-lib"] path = lib/arduino-lib url = https://github.com/arduino/arduino-lib-mpy.git +[submodule "lib/alif_ensemble-cmsis-dfp"] + path = lib/alif_ensemble-cmsis-dfp + url = https://github.com/alifsemi/alif_ensemble-cmsis-dfp.git +[submodule "lib/alif-security-toolkit"] + path = lib/alif-security-toolkit + url = https://github.com/micropython/alif-security-toolkit.git [submodule "user_modules/lv_binding_micropython"] path = user_modules/lv_binding_micropython url = https://github.com/lvgl/lv_binding_micropython.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a1c811339ad..ac9785bb592 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,14 +12,14 @@ repos: verbose: true stages: [commit-msg] - repo: https://github.com/charliermarsh/ruff-pre-commit - # Version should be kept in sync with .github/workflows/ruff.yml - rev: v0.1.3 + # Version should be kept in sync with .github/workflows/ruff.yml & also micropython-lib + rev: v0.11.6 hooks: - id: ruff - id: ruff-format - repo: https://github.com/codespell-project/codespell # Version should be kept in sync with .github/workflows/codespell.yml - rev: v2.2.6 + rev: v2.4.1 hooks: - id: codespell name: Spellcheck for changed files (codespell) diff --git a/CODECONVENTIONS.md b/CODECONVENTIONS.md index d6af0418e42..d3f71cb083d 100644 --- a/CODECONVENTIONS.md +++ b/CODECONVENTIONS.md @@ -206,14 +206,21 @@ adhere to the existing style and use `tools/codeformat.py` to check any changes. The main conventions, and things not enforceable via the auto-formatter, are described below. -White space: +As the MicroPython code base is over ten years old, not every source file +conforms fully to these conventions. If making small changes to existing code, +then it's usually acceptable to follow the existing code's style. New code or +major changes should follow the conventions described here. + +## White space + - Expand tabs to 4 spaces. - Don't leave trailing whitespace at the end of a line. - For control blocks (if, for, while), put 1 space between the keyword and the opening parenthesis. - Put 1 space after a comma, and 1 space around operators. -Braces: +## Braces + - Use braces for all blocks, even no-line and single-line pieces of code. - Put opening braces on the end of the line it belongs to, not on @@ -221,18 +228,43 @@ Braces: - For else-statements, put the else on the same line as the previous closing brace. -Header files: +## Header files + - Header files should be protected from multiple inclusion with #if directives. See an existing header for naming convention. -Names: +## Names + - Use underscore_case, not camelCase for all names. - Use CAPS_WITH_UNDERSCORE for enums and macros. - When defining a type use underscore_case and put '_t' after it. -Integer types: MicroPython runs on 16, 32, and 64 bit machines, so it's -important to use the correctly-sized (and signed) integer types. The -general guidelines are: +### Public names (declared in headers) + +- MicroPython-specific names (especially any declared in `py/` and `extmod/` + directories) should generally start with `mp_` or `MP_`. +- Functions and variables declared in a header should generally share a longer + common prefix. Usually the prefix matches the file name (i.e. items defined in + `py/obj.c` are declared in `py/obj.h` and should be prefixed `mp_obj_`). There + are exceptions, for example where one header file contains declarations + implemented in multiple source files for expediency. + +### Private names (specific to a single .c file) + +- For static functions and variables exposed to Python (i.e. a static C function + that is wrapped in `MP_DEFINE_CONST_FUN_...` and attached to a module), use + the file-level shared common prefix, i.e. name them as if the function or + variable was not static. +- Other static definitions in source files (i.e. functions or variables defined + in a .c file that are only used within that .c file) don't need any prefix + (specifically: no `s_` or `_` prefix, and generally avoid adding the + file-level common prefix). + +## Integer types + +MicroPython runs on 16, 32, and 64 bit machines, so it's important to use the +correctly-sized (and signed) integer types. The general guidelines are: + - For most cases use mp_int_t for signed and mp_uint_t for unsigned integer values. These are guaranteed to be machine-word sized and therefore big enough to hold the value from a MicroPython small-int @@ -241,11 +273,13 @@ general guidelines are: - You can use int/uint, but remember that they may be 16-bits wide. - If in doubt, use mp_int_t/mp_uint_t. -Comments: +## Comments + - Be concise and only write comments for things that are not obvious. - Use `// ` prefix, NOT `/* ... */`. No extra fluff. -Memory allocation: +## Memory allocation + - Use m_new, m_renew, m_del (and friends) to allocate and free heap memory. These macros are defined in py/misc.h. diff --git a/LICENSE b/LICENSE index 550ed9574d0..28b5239e5fe 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-2024 Damien P. George +Copyright (c) 2013-2026 Damien P. George Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 566a0f75dd1..6558a59888d 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,10 @@ The MicroPython project MicroPython Logo

-This is the MicroPython project, which aims to put an implementation -of Python 3.x on microcontrollers and small embedded systems. +This is the MicroPython project, an implementation of Python 3.x for +microcontrollers, embedded systems and other constrained platforms. You can find the official website at [micropython.org](http://www.micropython.org). -WARNING: this project is in beta stage and is subject to changes of the -code-base, including project-wide name changes and API changes. - MicroPython implements the entire Python 3.4 syntax (including exceptions, `with`, `yield from`, etc., and additionally `async`/`await` keywords from Python 3.5 and some select features from later versions). The following core @@ -59,6 +56,44 @@ the officially supported board from the see the [schematics and pinouts](http://github.com/micropython/pyboard) and [documentation](https://docs.micropython.org/en/latest/pyboard/quickref.html). +MicroPython design values +------------------------- + +"Perfection is achieved, not when there is nothing more to add, but when there +is nothing left to take away." ―- Antoine de Saint-Exupéry. + +For its design and implementation, MicroPython aims to follow a set of values. +Although not a strict set of rules, these values and principles serve as a +useful guide for new and seasoned contributors, as well as maintainers. + +MicroPython is at heart a combination of "Micro" and "Python": it's about +resource constrained systems running the Python programming language. Both of +these concepts balance off against each other in all parts of MicroPython's +design and implementation. + +The key concepts that focus the development of MicroPython are: +- Minimalism: do lots with little. +- Efficiency: engineering, build, execution, storage, power consumption. +- Consistency: the whole system feels like it was designed at once. + +When using MicroPython, the Python language is used as the human interface to a +system, giving fine control over the entities attached to that system. +In a hardware setting, MicroPython aims to give the user a bare-metal feeling: +one should feel like they have complete control over the system, with very +little between the programmer and the physical world. + +MicroPython recognises that systems can be very complex. The existing Python +libraries in combination with the MicroPython-specific libraries provide a +user-friendly way to harness the complexity of a system. + +Python language compatibility is very important to MicroPython, and at first +glance MicroPython should look just like regular Python. In the first instance, +most Python scripts should run unchanged on MicroPython, even on devices with very +tight resources. Beyond that, there are ways to extend MicroPython if needed to +better match Python. The provided built-in modules are an efficient subset of +the corresponding Python ones, without duplication of functionality, and allow +extension in Python if needed. + Contributing ------------ @@ -86,9 +121,8 @@ This repository contains the following components: - [examples/](examples/) -- a few example Python scripts. "make" is used to build the components, or "gmake" on BSD-based systems. -You will also need bash, gcc, and Python 3.3+ available as the command `python3` -(if your system only has Python 2.7 then invoke make with the additional option -`PYTHON=python2`). Some ports (rp2 and esp32) additionally use CMake. +You will also need bash, gcc, and Python 3.3+ available as the command `python3`. +Some ports (rp2 and esp32) additionally use CMake. Supported platforms & architectures ----------------------------------- @@ -105,28 +139,74 @@ development and testing of MicroPython itself, as well as providing lightweight alternative to CPython on these platforms (in particular on embedded Linux systems). -The ["minimal"](ports/minimal) port provides an example of a very basic -MicroPython port and can be compiled as both a standalone Linux binary as -well as for ARM Cortex M4. Start with this if you want to port MicroPython to -another microcontroller. Additionally the ["bare-arm"](ports/bare-arm) port -is an example of the absolute minimum configuration, and is used to keep -track of the code size of the core runtime and VM. - -In addition, the following ports are provided in this repository: - - [cc3200](ports/cc3200) -- Texas Instruments CC3200 (including PyCom WiPy). - - [esp32](ports/esp32) -- Espressif ESP32 SoC (including ESP32S2, ESP32S3, ESP32C3, ESP32C6). - - [esp8266](ports/esp8266) -- Espressif ESP8266 SoC. - - [mimxrt](ports/mimxrt) -- NXP m.iMX RT (including Teensy 4.x). - - [nrf](ports/nrf) -- Nordic Semiconductor nRF51 and nRF52. - - [pic16bit](ports/pic16bit) -- Microchip PIC 16-bit. - - [powerpc](ports/powerpc) -- IBM PowerPC (including Microwatt) - - [qemu](ports/qemu) -- QEMU-based emulated target (for testing) - - [renesas-ra](ports/renesas-ra) -- Renesas RA family. - - [rp2](ports/rp2) -- Raspberry Pi RP2040 (including Pico and Pico W). - - [samd](ports/samd) -- Microchip (formerly Atmel) SAMD21 and SAMD51. - - [stm32](ports/stm32) -- STMicroelectronics STM32 family (including F0, F4, F7, G0, G4, H7, L0, L4, WB) - - [webassembly](ports/webassembly) -- Emscripten port targeting browsers and NodeJS. - - [zephyr](ports/zephyr) -- Zephyr RTOS. +Over twenty different MicroPython ports are provided in this repository, +split across three +[MicroPython Support Tiers](https://docs.micropython.org/en/latest/develop/support_tiers.html). + +Tier 1 Ports +============ + +👑 Ports in [Tier 1](https://docs.micropython.org/en/latest/develop/support_tiers.html) +are mature and have the most active development, support and testing: + +| Port | Target | Quick Reference | +|--------------------------|----------------------------------------------------------------------------------------|----------------------------------------------------------------------| +| [esp32](ports/esp32)* | Espressif ESP32 SoCs (ESP32, ESP32S2, ESP32S3, ESP32C3, ESP32C6) | [here](https://docs.micropython.org/en/latest/esp32/quickref.html) | +| [mimxrt](ports/mimxrt) | NXP m.iMX RT | [here](https://docs.micropython.org/en/latest/mimxrt/quickref.html) | +| [rp2](ports/rp2) | Raspberry Pi RP2040 and RP2350 | [here](https://docs.micropython.org/en/latest/rp2/quickref.html) | +| [samd](ports/samd) | Microchip (formerly Atmel) SAMD21 and SAMD51 | [here](https://docs.micropython.org/en/latest/samd/quickref.html) | +| [stm32](ports/stm32) | STMicroelectronics STM32 MCUs (F0, F4, F7, G0, G4, H5, H7, L0, L1, L4, N6, WB, WL) | [here](https://docs.micropython.org/en/latest/pyboard/quickref.html) | +| [unix](ports/unix) | Linux, BSD, macOS, WSL | [here](https://docs.micropython.org/en/latest/unix/quickref.html) | +| [windows](ports/windows) | Microsoft Windows | [here](https://docs.micropython.org/en/latest/unix/quickref.html) | + +An asterisk indicates that the port has ongoing financial support from the vendor. + +Tier 2 Ports +============ + +✔ Ports in [Tier 2](https://docs.micropython.org/en/latest/develop/support_tiers.html) +are less mature and less actively developed and tested than Tier 1, but +still fully supported: + +| Port | Target | Quick Reference | +|----------------------------------|-------------------------------------------------------------|-------------------------------------------------------------------------| +| [alif](ports/alif) | Alif Semiconductor Ensemble MCUs (E3, E7) | | +| [embed](ports/embed) | Generates a set of .c/.h files for embedding into a project | | +| [nrf](ports/nrf) | Nordic Semiconductor nRF51 and nRF52 | | +| [renesas-ra](ports/renesas-ra) | Renesas RA family | [here](https://docs.micropython.org/en/latest/renesas-ra/quickref.html) | +| [webassembly](ports/webassembly) | Emscripten port targeting browsers and NodeJS | | +| [zephyr](ports/zephyr) | Zephyr RTOS | [here](https://docs.micropython.org/en/latest/zephyr/quickref.html) | + +Tier 3 Ports +============ + +Ports in [Tier 3](https://docs.micropython.org/en/latest/develop/support_tiers.html) +are built in CI but not regularly tested by the MicroPython maintainers: + +| Port | Target | Quick Reference | +|----------------------------|-------------------------------------------------------------------|-------------------------------------------------------------------------| +| [cc3200](ports/cc3200) | Texas Instruments CC3200 | [For WiPy](https://docs.micropython.org/en/latest/wipy/quickref.html) | +| [esp8266](ports/esp8266) | Espressif ESP8266 SoC | [here](https://docs.micropython.org/en/latest/esp8266/quickref.html) | +| [pic16bit](ports/pic16bit) | Microchip PIC 16-bit | | +| [powerpc](ports/powerpc) | IBM PowerPC (including Microwatt) | | + +Additional Ports +================ + +In addition to the above there is a Tier M containing ports that are used +primarily for maintenance, development and testing: + +- The ["bare-arm"](ports/bare-arm) port is an example of the absolute minimum + configuration that still includes the compiler, and is used to keep track + of the code size of the core runtime and VM. + +- The ["minimal"](ports/minimal) port provides an example of a very basic + MicroPython port and can be compiled as both a standalone Linux binary as + well as for ARM Cortex-M4. Start with this if you want to port MicroPython + to another microcontroller. + +- The [qemu](ports/qemu) port is a QEMU-based emulated target for Cortex-A, + Cortex-M, RISC-V 32-bit and RISC-V 64-bit architectures. The MicroPython cross-compiler, mpy-cross ----------------------------------------- diff --git a/docs/README.md b/docs/README.md index 892726ba17f..9b3b036e063 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,8 +13,7 @@ Building the documentation locally If you're making changes to the documentation, you may want to build the documentation locally so that you can preview your changes. -Install Sphinx, and optionally (for the RTD-styling), sphinx_rtd_theme, -preferably in a virtualenv: +Install Sphinx and sphinx_rtd_theme, preferably in a virtualenv: pip install sphinx pip install sphinx_rtd_theme @@ -25,6 +24,21 @@ In `micropython/docs`, build the docs: You'll find the index page at `micropython/docs/build/html/index.html`. +Documentation autobuild +----------------------- + +For a more convenient development experience, you can use `sphinx-autobuild` +to automatically rebuild and serve the documentation when you make changes: + + pip install sphinx-autobuild + +Then run from the `micropython/docs` directory: + + sphinx-autobuild . build/html + +This will start a local web server (typically at `http://127.0.0.1:8000`) +and automatically rebuild the documentation whenever you save changes to the source files. + Having readthedocs.org build the documentation ---------------------------------------------- diff --git a/docs/conf.py b/docs/conf.py index 32cc2be10e3..f80ca97edca 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -36,6 +36,9 @@ "is_release": micropy_version != "latest", } +# Authors used in various parts of the documentation. +micropy_authors = "MicroPython authors and contributors" + # -- General configuration ------------------------------------------------ @@ -52,6 +55,7 @@ "sphinx.ext.todo", "sphinx.ext.coverage", "sphinxcontrib.jquery", + "sphinx_rtd_theme", ] # Add any paths that contain templates here, relative to this directory. @@ -68,7 +72,7 @@ # General information about the project. project = "MicroPython" -copyright = "- The MicroPython Documentation is Copyright © 2014-2024, Damien P. George, Paul Sokolovsky, and contributors" +copyright = "- The MicroPython Documentation is Copyright © 2014-2026, " + micropy_authors # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -125,20 +129,9 @@ # -- Options for HTML output ---------------------------------------------- -# on_rtd is whether we are on readthedocs.org -on_rtd = os.environ.get("READTHEDOCS", None) == "True" - -if not on_rtd: # only import and set the theme if we're building docs locally - try: - import sphinx_rtd_theme +import sphinx_rtd_theme - html_theme = "sphinx_rtd_theme" - html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."] - except: - html_theme = "default" - html_theme_path = ["."] -else: - html_theme_path = ["."] +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -244,7 +237,7 @@ master_doc, "MicroPython.tex", "MicroPython Documentation", - "Damien P. George, Paul Sokolovsky, and contributors", + micropy_authors, "manual", ), ] @@ -281,7 +274,7 @@ "index", "micropython", "MicroPython Documentation", - ["Damien P. George, Paul Sokolovsky, and contributors"], + [micropy_authors], 1, ), ] @@ -300,7 +293,7 @@ master_doc, "MicroPython", "MicroPython Documentation", - "Damien P. George, Paul Sokolovsky, and contributors", + micropy_authors, "MicroPython", "One line description of project.", "Miscellaneous", diff --git a/docs/develop/cmodules.rst b/docs/develop/cmodules.rst index c5aa919b764..ec20e65f477 100644 --- a/docs/develop/cmodules.rst +++ b/docs/develop/cmodules.rst @@ -8,8 +8,8 @@ limitations with the Python environment, often due to an inability to access certain hardware resources or Python speed limitations. If your limitations can't be resolved with suggestions in :ref:`speed_python`, -writing some or all of your module in C (and/or C++ if implemented for your port) -is a viable option. +writing some or all of your module in C (and/or +:ref:`C++ if implemented for your port`) is a viable option. If your module is designed to access or work with commonly available hardware or libraries please consider implementing it inside the MicroPython @@ -285,3 +285,73 @@ can now be accessed in Python just like any other builtin module, e.g. sleep_ms(1000) print(watch.time()) # should display approximately 1000 + + +.. _c_heap: + +C Dynamic Memory Allocation +--------------------------- + +MicroPython uses its own "Python heap" for `memorymanagement`, +which is not the same as the "C heap" used by C library functions ``malloc()``, +``free()``, etc. Not every MicroPython port comes with a "C heap" at all. + +Tier 1 & 2 ports have varying support for C dynamic memory allocation via a "C +heap": + +- unix, windows, esp32 and webassembly ports support C dynamic memory + allocation. +- rp2 port will fail to allocate any memory at runtime unless the firmware is + built with ``MICROPY_C_HEAP_SIZE=n`` to reserve ``n`` bytes of memory for a C + heap. This memory will not be available for Python code to use. +- alif, mimxrt, nrf, renesas-ra, samd, and stm32 port builds that include + dynamic C allocation will fail at link-time with errors such as ``undefined + reference to `malloc'``. MicroPython has no built-in support for dynamic C + allocation on these ports. Any solution requires manually adding a C heap + implementation to the custom build. +- zephyr port currently does not support building with user modules. + +Python heap as C heap +~~~~~~~~~~~~~~~~~~~~~ + +It may be practical for C code to call "Python heap" dynamic allocation +functions such ``m_malloc()``, ``m_malloc0()`` and ``m_free()`` instead. + +See `python_memory_from_c` for more information about this approach. + +.. _cxx_support: + +C++ Modules +----------- + +Most Tier 1 & 2 MicroPython ports (and some Tier 3) support building C++ user +modules, using the C++-specific environment variables described above. + +Integrating C++ and MicroPython successfully involves some additional +considerations: + +C++ Dynamic Memory Allocation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +C++ programs (as well as C++ Standard Library features) typically use dynamic +memory allocation. The C++ default memory allocator (i.e. operators ``new`` and +``delete``) is typically implemented as a layer on top of `c_heap`. + +For MicroPython ports which don't include C dynamic memory allocation support, +C++ dynamic memory allocation can be supported in one of two ways: + +- Implement C dynamic memory allocation in your custom build. +- Implement a custom C++ allocator in your custom build. + +Linkage Considerations +~~~~~~~~~~~~~~~~~~~~~~ + +Because MicroPython is a C-based project, any symbols which link to or from +MicroPython need to be qualified ``extern "C"`` in C++ code. + +It's strongly recommended to follow the pattern demonstrated in +`examples/usercmodule/cppexample +`_, +where the Python module is implemented in a minimal C file wrapper around the +C++ code. + diff --git a/docs/develop/gettingstarted.rst b/docs/develop/gettingstarted.rst index c1fd338c54c..a6afc5cad84 100644 --- a/docs/develop/gettingstarted.rst +++ b/docs/develop/gettingstarted.rst @@ -106,7 +106,7 @@ See the `ARM GCC toolchain `_ for the latest details. -Python is also required. Python 2 is supported for now, but we recommend using Python 3. +Python 3 is also required. Check that you have Python available on your system: .. code-block:: bash @@ -278,10 +278,86 @@ To run a selection of tests on a board/device connected over USB use: .. code-block:: bash $ cd tests - $ ./run-tests.py --target minimal --device /dev/ttyACM0 + $ ./run-tests.py -t /dev/ttyACM0 See also :ref:`writingtests`. +Additional make targets for developers +-------------------------------------- + +In all ``make``-based ports, there is a target to print the size of a specific object file. +When a change is confined to a single file, this is useful when testing variations to find smaller alternatives. + +For instance, to print the size of ``objstr.o`` in the ``py/`` directory when making a unix standard build: + +.. code-block:: bash + + $ make build-standard/py/objstr.sz + +Similarly, there is a target to save the preprocessed version of a file: + +.. code-block:: bash + + $ make build-standard/py/objstr.pp + +In ``ports/unix`` there are additional targets related to running tests: + +.. code-block:: bash + + $ make test//int # Run all tests matching the pattern "int" + $ make test/ports/unix # Run all tests in ports/unix + $ make test-failures # Re-run only the failed tests + $ make print-failures # print the differences for failed tests + $ make clean-failures # delete the .exp and .out files from failed tests + +Using ci.sh locally +------------------- + +MicroPython uses GitHub Actions for continuous integration. +To reduce dependence on any specific CI system, the actual build steps for Unix-based builds are in the file ``tools/ci.sh``. +This can also be used as a script on developer desktops, with caveats: + +* For most steps, An Ubuntu/Debian system similar to the one used during CI is assumed. +* Some specific steps assume specific Ubuntu versions. +* The setup steps may invoke the system package manager to install packages, + download and install software from the internet, etc. + +To get a usage message including the list of commands, run: + +.. code-block:: bash + + $ tools/ci.sh --help + +As an example, you can build and test the unix minimal port with: + +.. code-block:: bash + + $ tools/ci.sh unix_minimal_build unix_minimal_run_tests + +If you use the bash shell, you can add a ``ci`` command with tab completion: + +.. code-block:: bash + + $ eval $(tools/ci.sh --bash-completion) + +For the zsh shell, replace ``--bash-completion`` with ``--zsh-completion``. +For the fish shell, replace ``--bash-completion`` with ``--fish-completion``. + +Then, typing: + +.. code-block:: bash + + $ ci unix_cov + +This will complete the ci step name to ``unix_coverage_``. +Pressing tab a second time will show the list of matching steps: + +.. code-block:: bash + + $ ci unix_coverage_ + unix_coverage_32bit_build + unix_coverage_32bit_run_native_mpy_tests… + Folder structure ---------------- diff --git a/docs/develop/index.rst b/docs/develop/index.rst index 327038f1978..1026b43d0c3 100644 --- a/docs/develop/index.rst +++ b/docs/develop/index.rst @@ -24,3 +24,4 @@ MicroPython to a new platform and implementing a core MicroPython library. publiccapi.rst extendingmicropython.rst porting.rst + support_tiers.rst diff --git a/docs/develop/library.rst b/docs/develop/library.rst index 830211d81c8..6ff9bfe023d 100644 --- a/docs/develop/library.rst +++ b/docs/develop/library.rst @@ -26,15 +26,9 @@ Implementing a core module -------------------------- Like CPython, MicroPython has core builtin modules that can be accessed through import statements. -An example is the ``gc`` module discussed in :ref:`memorymanagement`. +An example is the :mod:`gc` module discussed in :ref:`memorymanagement`. -.. code-block:: bash - - >>> import gc - >>> gc.enable() - >>> - -MicroPython has several other builtin standard/core modules like ``io``, ``array`` etc. +MicroPython has several other builtin standard/core modules like :mod:`io`, :mod:`array`, etc. Adding a new core module involves several modifications. First, create the ``C`` file in the ``py/`` directory. In this example we are adding a diff --git a/docs/develop/memorymgt.rst b/docs/develop/memorymgt.rst index 5b1690cc827..4ae80120044 100644 --- a/docs/develop/memorymgt.rst +++ b/docs/develop/memorymgt.rst @@ -4,43 +4,80 @@ Memory Management ================= Unlike programming languages such as C/C++, MicroPython hides memory management -details from the developer by supporting automatic memory management. -Automatic memory management is a technique used by operating systems or applications to automatically manage -the allocation and deallocation of memory. This eliminates challenges such as forgetting to -free the memory allocated to an object. Automatic memory management also avoids the critical issue of using memory -that is already released. Automatic memory management takes many forms, one of them being -garbage collection (GC). - -The garbage collector usually has two responsibilities; - -#. Allocate new objects in available memory. -#. Free unused memory. - -There are many GC algorithms but MicroPython uses the -`Mark and Sweep `_ -policy for managing memory. This algorithm has a mark phase that traverses the heap marking all -live objects while the sweep phase goes through the heap reclaiming all unmarked objects. - -Garbage collection functionality in MicroPython is available through the ``gc`` built-in -module: - -.. code-block:: bash - - >>> x = 5 - >>> x - 5 - >>> import gc - >>> gc.enable() - >>> gc.mem_alloc() - 1312 - >>> gc.mem_free() - 2071392 - >>> gc.collect() - 19 - >>> gc.disable() - >>> - -Even when ``gc.disable()`` is invoked, collection can be triggered with ``gc.collect()``. +details from the developer by supporting automatic memory management of a +":ref:`Python heap`" that holds all Python objects. MicroPython uses +garbage collection (GC) for automatic memory management. The garbage collector +is responsible for freeing memory which is no longer in use. + +Specifically, MicroPython uses a `Mark and Sweep +`_ +garbage collection algorithm. This algorithm has a mark phase that scans the +heap marking all live objects, and then a sweep phase goes through the heap +reclaiming all unmarked objects. + +The MicroPython garbage collector is by default automatic, but manual control is +available through the :mod:`gc` built-in module. + +.. _python_memory_from_c: + +MicroPython Memory from C code +------------------------------ + +Awareness of the garbage collector is needed when writing C code that allocates +memory from the "Python heap" (i.e. functions ``m_malloc()``, ``m_malloc0()``, +``m_free()``, etc). + +The mark phase of the garbage collector scans for live pointers to heap memory +starting from the following roots: + +- The stack of the main Python runtime (or REPL). +- The stacks of each "Python thread", for ports which implement Python threads + on top of native operating system threads or tasks. +- The "root pointers" defined in C code using the macro + ``MP_REGISTER_ROOT_POINTER``. These are the recommended way to have statically + scoped pointers to the Python heap. +- Tracked allocations made with the ``m_tracked_calloc()``, ``m_tracked_realloc`` + and ``m_tracked_free()`` functions. These special functions allow allocating a + block of memory which is always considered live by the garbage collector. + Similar to memory allocation in C, this memory is only freed by calling + ``m_tracked_free()`` or by soft reset. There is a small memory usage and + runtime overhead to each tracked allocation. This feature is not enabled by + default on all ports. + +The garbage collector then recursively scans and marks all the memory pointed to +by the root pointers, until all addresses are exhausted. This is sufficient to +find all Python objects that are still in use by the MicroPython runtime. + +However, the following memory will **not** be scanned by the garbage collector +and could be freed prematurely: + +- Static or global C variables which contain pointers to heap memory. +- Pointers which don't point to the "head" of an allocated buffer (i.e. to the + exact address returned by ``m_malloc()``), but instead to an address inside + the allocated buffer (for example, a pointer to a nested struct). For + performance reasons, the garbage collector doesn't mark the enclosing buffer + in these cases. +- The stack of any thread or RTOS task which isn't running Python code or + manually registered as a "Python thread" (for ports which support native + threads or tasks). + +Ways to avoid use-after-free in these scenarios: + +- Use the tracked allocation API ``m_tracked_calloc()``, ``m_tracked_realloc()`` + and ``m_tracked_free()``. +- Register a root pointer (see above), instead of storing a pointer in a static + variable. +- Restructure the code, for example by having an API where Python code + initialises a singleton Python object (implemented in C) which holds all of the + relevant pointers instead of having them in static variables. + +.. note:: :ref:`soft_reset` always clears the Python heap and frees all memory. + It's important not to hold any pointers to the heap after a soft + reset, as they will become dangling pointers to freed memory. + + Some ports support a "C heap" as well (see `c_heap`), in which case + you can allocate memory that will stay valid over soft reset by + calling standard C functions ``malloc``, etc. The object model ---------------- diff --git a/docs/develop/natmod.rst b/docs/develop/natmod.rst index 502ea1c4c68..e0f7bdaaa89 100644 --- a/docs/develop/natmod.rst +++ b/docs/develop/natmod.rst @@ -39,11 +39,18 @@ options for the ``ARCH`` variable, see below): * ``armv7emsp`` (ARM Thumb 2, single precision float, eg Cortex-M4F, Cortex-M7) * ``armv7emdp`` (ARM Thumb 2, double precision float, eg Cortex-M7) * ``xtensa`` (non-windowed, eg ESP8266) -* ``xtensawin`` (windowed with window size 8, eg ESP32) +* ``xtensawin`` (windowed with window size 8, eg ESP32, ESP32S3) +* ``rv32imc`` (RISC-V 32 bits with compressed instructions, eg ESP32C3, ESP32C6) +* ``rv64imc`` (RISC-V 64 bits with compressed instructions) + +If the chosen platform supports explicit architecture flags and you want to let +the output .mpy file carry those flags' value, you must pass them to the +``ARCH_FLAGS`` flags variable when building the .mpy file. When compiling and linking the native .mpy file the architecture must be chosen -and the corresponding file can only be imported on that architecture. For more -details about .mpy files see :ref:`mpy_files`. +and the corresponding file can only be imported on that architecture (and if +architecture flags are present, only if they match the target's capabilities). +For more details about .mpy files see :ref:`mpy_files`. Native code must be compiled as position independent code (PIC) and use a global offset table (GOT), although the details of this varies from architecture to @@ -66,14 +73,31 @@ The known limitations are: * static BSS variables are not supported; workaround: use global BSS variables +* thread-local storage variables are not supported on rv32imc; workaround: use + global BSS variables or allocate some space on the heap to store them + So, if your C code has writable data, make sure the data is defined globally, without an initialiser, and only written to within functions. +The native module is not automatically linked against the standard static libraries +like ``libm.a`` and ``libgcc.a``, which can lead to ``undefined symbol`` errors. +You can link the runtime libraries by setting ``LINK_RUNTIME = 1`` +in your Makefile. Custom static libraries can also be linked by adding +``MPY_LD_FLAGS += -l path/to/library.a``. Note that these are linked into +the native module and will not be shared with other modules or the system. + Linker limitation: the native module is not linked against the symbol table of the full MicroPython firmware. Rather, it is linked against an explicit table of exported symbols found in ``mp_fun_table`` (in ``py/nativeglue.h``), that is fixed at firmware build time. It is thus not possible to simply call some arbitrary HAL/OS/RTOS/system -function, for example. +function, for example, unless that resides at a fixed address. In that case, the path +of a linkerscript containing a series of symbol names and their fixed address can be +passed to ``mpy_ld.py`` via the ``--externs`` command line argument. That way symbols +appearing in the linkerscript will take precedence over what is provided from object +files, but at the moment the object files' implementation will still reside in the +final MPY file. The linkerscript parser is limited in its capabilities, and is +currently used only for parsing the ESP8266 port ROM symbols list (see +``ports/esp8266/boards/eagle.rom.addr.v6.ld``). New symbols can be added to the end of the table and the firmware rebuilt. The symbols also need to be added to ``tools/mpy_ld.py``'s ``fun_table`` dict in the @@ -105,7 +129,8 @@ The filesystem layout consists of two main parts, the source files and the Makef location of the MicroPython repository (to find header files, the relevant Makefile fragment, and the ``mpy_ld.py`` tool), ``MOD`` as the name of the module, ``SRC`` as the list of source files, optionally specify the machine architecture via ``ARCH``, - and then include ``py/dynruntime.mk``. + along with optional machine architecture flags specified via ``ARCH_FLAGS``, and + then include ``py/dynruntime.mk``. Minimal example --------------- @@ -172,7 +197,7 @@ The file ``Makefile`` contains: # Source files (.c or .py) SRC = factorial.c - # Architecture to build for (x86, x64, armv6m, armv7m, xtensa, xtensawin) + # Architecture to build for (x86, x64, armv6m, armv7m, xtensa, xtensawin, rv32imc, rv64imc) ARCH = x64 # Include to get the rules for compiling and linking the module @@ -198,6 +223,10 @@ Without modifying the Makefile you can specify the target architecture via:: $ make ARCH=armv7m +Same applies for optional architecture flags via:: + + $ make ARCH=rv32imc ARCH_FLAGS=zba + Module usage in MicroPython --------------------------- @@ -210,6 +239,26 @@ other module, for example:: print(factorial.factorial(10)) # should display 3628800 +Using Picolibc when building modules +------------------------------------ + +Using `Picolibc `_ as your C standard +library is not only supported, but in fact it is the default for the rv32imc and +rv64imc platforms. However, there are a couple of things worth mentioning to make +sure you don't run into problems later when building code. + +Some pre-built Picolibc versions (for example, those provided by Ubuntu Linux +as the ``picolibc-arm-none-eabi``, ``picolibc-riscv64-unknown-elf``, and +``picolibc-xtensa-lx106-elf`` packages) assume thread-local storage (TLS) is +available at runtime, but unfortunately MicroPython modules do not support that +on some architectures (namely ``rv32imc`` and ``rv64imc``). This means that some +functionalities provided by Picolibc will default to use TLS, returning an +error either during compilation or during linking. + +For an example on how this may affect you, the ``examples/natmod/btree`` +example module contains a workaround to make sure ``errno`` works (look for +``__PICOLIBC_ERRNO_FUNCTION`` in the Makefile and follow the trail from there). + Further examples ---------------- diff --git a/docs/develop/porting.rst b/docs/develop/porting.rst index 99725e1000b..28d7b3dd513 100644 --- a/docs/develop/porting.rst +++ b/docs/develop/porting.rst @@ -42,7 +42,6 @@ The basic MicroPython firmware is implemented in the main port file, e.g ``main. #include "py/compile.h" #include "py/gc.h" #include "py/mperrno.h" - #include "py/stackctrl.h" #include "shared/runtime/gchelper.h" #include "shared/runtime/pyexec.h" @@ -51,7 +50,7 @@ The basic MicroPython firmware is implemented in the main port file, e.g ``main. int main(int argc, char **argv) { // Initialise the MicroPython runtime. - mp_stack_ctrl_init(); + mp_cstack_init_with_sp_here(2048); gc_init(heap, heap + sizeof(heap)); mp_init(); @@ -162,8 +161,6 @@ The following is an example of an ``mpconfigport.h`` file: // Type definitions for the specific machine. - typedef intptr_t mp_int_t; // must be pointer size - typedef uintptr_t mp_uint_t; // must be pointer size typedef long mp_off_t; // We need to provide a declaration/definition of alloca(). @@ -244,10 +241,12 @@ That should give a MicroPython REPL. You can then run commands like: .. code-block:: bash - MicroPython v1.13 on 2021-01-01; example-board with unknown-cpu - >>> import sys - >>> sys.implementation - ('micropython', (1, 13, 0)) + MicroPython v1.26.0-preview on 2025-08-01; minimal with unknown-cpu + >>> def sum(n, m): + ... return n + m + ... + >>> 3, 4, sum(3, 4) + (3, 4, 7) >>> Use Ctrl-D to exit, and then run ``reset`` to reset the terminal. diff --git a/docs/develop/support_tiers.rst b/docs/develop/support_tiers.rst new file mode 100644 index 00000000000..f49ee4124f8 --- /dev/null +++ b/docs/develop/support_tiers.rst @@ -0,0 +1,68 @@ +MicroPython Support Tiers +========================= + +MicroPython operates with a set of Support Tier levels for the various ports. +Tiers 1, 2 and 3 are the main Tier levels with Tier 1 being the most mature and +actively maintained. There is also Tier M for additional ports used primarily +for maintenance, development and testing. These Tier levels are defined in the +table below. + +.. table:: + :widths: 40 9 9 9 9 + + +-----------------------------------------------+--------+--------+--------+--------+ + | | Tier 1 | Tier 2 | Tier 3 | Tier M | + +-----------------------------------------------+--------+--------+--------+--------+ + | builds pass under CI | ✔ | ✔ | ✔ | ✔ | + +-----------------------------------------------+--------+--------+--------+--------+ + | tests run under CI (where possible) | ✔ | ✔ | ✔ | ✔ | + +-----------------------------------------------+--------+--------+--------+--------+ + | actively maintained | ✔ | ✔ | | ✔ | + +-----------------------------------------------+--------+--------+--------+--------+ + | stable Python API | ✔ | ✔ | | | + +-----------------------------------------------+--------+--------+--------+--------+ + | new features actively developed | ✔ | ✔ | | | + +-----------------------------------------------+--------+--------+--------+--------+ + | tested on hardware for releases | ✔ | ✔ | | | + +-----------------------------------------------+--------+--------+--------+--------+ + | prioritized bug reports | ✔ | | | ✔ | + +-----------------------------------------------+--------+--------+--------+--------+ + | regressions warrant a patch release | ✔ | | | ✔ | + +-----------------------------------------------+--------+--------+--------+--------+ + | has port-specific documentation | ✔ | | | | + +-----------------------------------------------+--------+--------+--------+--------+ + +Lower Tiers may tick more boxes, but the above table defines the minimum requirements +for a port to belong to a Tier. + +Tier 1 ports: + + - esp32 + - mimxrt + - rp2 + - samd + - stm32 + - unix + - windows + +Tier 2 ports: + + - alif + - embed + - nrf + - renesas-ra + - webassembly + - zephyr + +Tier 3 ports: + + - cc3200 + - esp8266 + - pic16bit + - powerpc + +Tier M ports: + + - bare-arm + - minimal + - qemu diff --git a/docs/develop/writingtests.rst b/docs/develop/writingtests.rst index 9bb5178f55f..fd3daf91c1e 100644 --- a/docs/develop/writingtests.rst +++ b/docs/develop/writingtests.rst @@ -60,7 +60,7 @@ Then to run on a board: .. code-block:: bash - $ ./run-tests.py --target minimal --device /dev/ttyACM0 + $ ./run-tests.py -t /dev/ttyACM0 And to run only a certain set of tests (eg a directory): @@ -68,3 +68,76 @@ And to run only a certain set of tests (eg a directory): $ ./run-tests.py -d basics $ ./run-tests.py float/builtin*.py + +Using run-tests.py +------------------ + +The ``run-tests.py`` script supports several parameters to customize test execution: + +**Target and Device Selection:** + +* ``-t, --test-instance`` + +The -t option accepts the following for the test instance: + +- **unix** - use the unix port of MicroPython, specified by the MICROPY_MICROPYTHON + environment variable (which defaults to the standard variant of either the unix + or windows ports, depending on the host platform) +- **webassembly** - use the webassembly port of MicroPython, specified by the + MICROPY_MICROPYTHON_MJS environment variable (which defaults to the standard + variant of the webassembly port) +- **port:** - connect to and use the given serial port device +- **a** - connect to and use /dev/ttyACM +- **u** - connect to and use /dev/ttyUSB +- **c** - connect to and use COM +- **exec:** - execute a command and attach to it's stdin/stdout +- **execpty:** - execute a command and attach to the printed /dev/pts/ device +- **...** - connect to the given IPv4 address +- anything else specifies a serial port + +**Test Selection:** + +* ``-d, --test-dirs`` - Specify one or more test directories to run +* ``-i, --include REGEX`` - Include tests matching regex pattern +* ``-e, --exclude REGEX`` - Exclude tests matching regex pattern +* ``files`` - Specific test files to run + +**Execution Options:** + +* ``--emit `` - MicroPython emitter, EMITTER can be bytecode or native. Default: bytecode +* ``--via-mpy`` - Compile .py files to .mpy first +* ``--heapsize`` - Set heap size for tests +* ``-j, --jobs N`` - Number of tests to run simultaneously + +Set the MICROPY_MPYCROSS environment variable to use a specific version of ``mpy-cross`` when using ``--via-mpy``. + +**Result Management:** + +* ``-r, --result-dir`` - Directory for test results. Default: results/ +* ``--print-failures`` - Show diff of failed tests and exit +* ``--clean-failures`` - Delete .exp and .out files from prior failed tests +* ``--run-failures`` - Re-run only previously failed tests + +**Examples:** + +.. code-block:: bash + + # Run only basic tests with native emitter + $ ./run-tests.py --emit native -d basics extmod + + # Run tests excluding async functionality + $ ./run-tests.py -e async + + # Run tests matching *_pep_* + $ ./run-tests.py -i *_pep_* + + # Run specific test files in parallel + $ ./run-tests.py -j 4 basics/list*.py + + # Test on connected ESP32 board + $ ./run-tests.py -t /dev/ttyUSB0 + # or + $ ./run-tests.py -t u0 + + # Re-run only failed tests from previous run + $ ./run-tests.py --run-failures diff --git a/docs/differences/python_36.rst b/docs/differences/python_36.rst index 3315b0594da..18da79f8f84 100644 --- a/docs/differences/python_36.rst +++ b/docs/differences/python_36.rst @@ -25,7 +25,8 @@ Python 3.6 beta 1 was released on 12 Sep 2016, and a summary of the new features +--------------------------------------------------------+--------------------------------------------------+-----------------+ | `PEP 468 `_ | Preserving the order of *kwargs* in a function | | +--------------------------------------------------------+--------------------------------------------------+-----------------+ - | `PEP 487 `_ | Simpler customization of class creation | | + | `PEP 487 `_ | Simpler customization of class creation | Partial | + | | | [#setname]_ | +--------------------------------------------------------+--------------------------------------------------+-----------------+ | `PEP 520 `_ | Preserving Class Attribute Definition Order | | +--------------------------------------------------------+--------------------------------------------------+-----------------+ @@ -198,3 +199,7 @@ Changes to built-in modules: +--------------------------------------------------------------------------------------------------------------+----------------+ | The *compress()* and *decompress()* functions now accept keyword arguments | | +--------------------------------------------------------------------------------------------------------------+----------------+ + +.. rubric:: Notes + +.. [#setname] Currently, only :func:`__set_name__` is implemented. diff --git a/docs/esp32/quickref.rst b/docs/esp32/quickref.rst index 3ab4e8f5ecd..b4961fd4ed4 100644 --- a/docs/esp32/quickref.rst +++ b/docs/esp32/quickref.rst @@ -79,34 +79,34 @@ Networking WLAN ^^^^ -The :mod:`network` module:: +The :class:`network.WLAN` class in the :mod:`network` module:: import network - wlan = network.WLAN(network.STA_IF) # create station interface - wlan.active(True) # activate the interface - wlan.scan() # scan for access points - wlan.isconnected() # check if the station is connected to an AP + wlan = network.WLAN() # create station interface (the default, see below for an access point interface) + wlan.active(True) # activate the interface + wlan.scan() # scan for access points + wlan.isconnected() # check if the station is connected to an AP wlan.connect('ssid', 'key') # connect to an AP - wlan.config('mac') # get the interface's MAC address - wlan.ipconfig('addr4') # get the interface's IPv4 addresses + wlan.config('mac') # get the interface's MAC address + wlan.ipconfig('addr4') # get the interface's IPv4 addresses - ap = network.WLAN(network.AP_IF) # create access-point interface - ap.config(ssid='ESP-AP') # set the SSID of the access point - ap.config(max_clients=10) # set how many clients can connect to the network - ap.active(True) # activate the interface + ap = network.WLAN(network.WLAN.IF_AP) # create access-point interface + ap.config(ssid='ESP-AP') # set the SSID of the access point + ap.config(max_clients=10) # set how many clients can connect to the network + ap.active(True) # activate the interface A useful function for connecting to your local WiFi network is:: def do_connect(): - import network - wlan = network.WLAN(network.STA_IF) + import machine, network + wlan = network.WLAN() wlan.active(True) if not wlan.isconnected(): print('connecting to network...') wlan.connect('ssid', 'key') while not wlan.isconnected(): - pass + machine.idle() print('network config:', wlan.ipconfig('addr4')) Once the network is established the :mod:`socket ` module can be used @@ -121,10 +121,20 @@ calling ``wlan.config(reconnects=n)``, where n are the number of desired reconne attempts (0 means it won't retry, -1 will restore the default behaviour of trying to reconnect forever). +.. _esp32_network_lan: + LAN ^^^ -To use the wired interfaces one has to specify the pins and mode :: +Built-in MAC (original ESP32) +""""""""""""""""""""""""""""" + +The original ESP32 SoC has a built-in Ethernet MAC. Using this MAC requires an +external Ethernet PHY to be wired to the chip's EMAC pins. Most of the EMAC pin +assignments are fixed, consult the ESP32 datasheet for details. + +If the PHY is connected, the internal Ethernet MAC can be configured via +the :class:`network.LAN` constructor:: import network @@ -133,20 +143,34 @@ To use the wired interfaces one has to specify the pins and mode :: lan.ipconfig('addr4') # get the interface's IPv4 addresses -The keyword arguments for the constructor defining the PHY type and interface are: +Required keyword arguments for the constructor: + +- ``mdc`` and ``mdio`` - :class:`machine.Pin` objects (or integers) specifying + the MDC and MDIO pins. +- ``phy_type`` - Select the PHY device type. Supported devices are + ``PHY_GENERIC``, + ``PHY_LAN8710``, ``PHY_LAN8720``, ``PHY_IP101``, ``PHY_RTL8201``, + ``PHY_DP83848``, ``PHY_KSZ8041`` and ``PHY_KSZ8081``. These values are all + constants defined in the ``network`` module. +- ``phy_addr`` - The address number of the PHY device. Must be an integer in the + range 0x00 to 0x1f, inclusive. Common values are ``0`` and ``1``. + +All of the above keyword arguments must be present to configure the interface. + +Optional keyword arguments: -- mdc=pin-object # set the mdc and mdio pins. -- mdio=pin-object -- reset=pin-object # set the reset pin of the PHY device. -- power=pin-object # set the pin which switches the power of the PHY device. -- phy_type= # Select the PHY device type. Supported devices are PHY_LAN8710, - PHY_LAN8720, PH_IP101, PHY_RTL8201, PHY_DP83848 and PHY_KSZ8041 -- phy_addr=number # The address number of the PHY device. -- ref_clk_mode=mode # Defines, whether the ref_clk at the ESP32 is an input - or output. Suitable values are Pin.IN and Pin.OUT. -- ref_clk=pin-object # defines the Pin used for ref_clk. +- ``reset`` - :class:`machine.Pin` object (or integer) specifying the PHY reset pin. +- ``power`` - :class:`machine.Pin` object (or integer) specifying a pin which + switches the power of the PHY device. +- ``ref_clk`` - :class:`machine.Pin` object (or integer) specifying the pin used + for the EMAC ``ref_clk`` signal. If not specified, the board default is used + (typically GPIO 0, but may be different if a particular board has Ethernet.) +- ``ref_clk_mode`` - Defines whether the EMAC ``ref_clk`` pin of the ESP32 + should be an input or an output. Suitable values are ``machine.Pin.IN`` and + ``machine.Pin.OUT``. If not specified, the board default is used + (typically input, but may be different if a particular board has Ethernet.) -These are working configurations for LAN interfaces of popular boards:: +These are working configurations for LAN interfaces of some popular ESP32 boards:: # Olimex ESP32-GATEWAY: power controlled by Pin(5) # Olimex ESP32 PoE and ESP32-PoE ISO: power controlled by Pin(12) @@ -171,6 +195,66 @@ These are working configurations for LAN interfaces of popular boards:: lan = network.LAN(id=0, mdc=Pin(23), mdio=Pin(18), power=Pin(5), phy_type=network.PHY_IP101, phy_addr=1) + +.. _esp32_spi_ethernet: + +SPI Ethernet Interface +"""""""""""""""""""""" + +All ESP32 SoCs support external SPI Ethernet interface chips. These are Ethernet +interfaces that connect via a SPI bus, rather than an Ethernet RMII interface. + +.. note:: The only exception is the ESP32 ``d2wd`` variant, where this feature is disabled + to save code size. + +SPI Ethernet uses the same :class:`network.LAN` constructor, with a different +set of keyword arguments:: + + import machine, network + + spi = machine.SPI(1, sck=SCK_PIN, mosi=MOSI_PIN, miso=MISO_PIN) + lan = network.LAN(spi=spi, cs=CS_PIN, ...) # Set the pin and mode configuration + lan.active(True) # activate the interface + lan.ipconfig('addr4') # get the interface's IPv4 addresses + +Required keyword arguments for the constructor: + +- ``spi`` - Should be a :class:`machine.SPI` object configured for this + connection. Note that any clock speed configured on the SPI object is ignored, + the SPI Ethernet clock speed is configured at compile time. +- ``cs`` - :class:`machine.Pin` object (or integer) specifying the CS pin + connected to the interface. +- ``int`` - :class:`machine.Pin` object (or integer) specifying the INT pin + connected to the interface. +- ``phy_type`` - Select the SPI Ethernet interface type. Supported devices are + ``PHY_KSZ8851SNL``, ``PHY_DM9051``, ``PHY_W5500``. These values are all + constants defined in the ``network`` module. +- ``phy_addr`` - The address number of the PHY device. Must be an integer in the + range 0x00 to 0x1f, inclusive. This is usually ``0`` for SPI Ethernet devices. + +All of the above keyword arguments must be present to configure the interface. + +Optional keyword arguments for the constructor: + +- ``reset`` - :class:`machine.Pin` object (or integer) specifying the SPI Ethernet + interface reset pin. +- ``power`` - :class:`machine.Pin` object (or integer) specifying a pin which + switches the power of the SPI Ethernet interface. + +Here is a sample configuration for a WIZNet W5500 chip connected to pins on +an ESP32-S3 development board:: + + import machine, network + from machine import Pin, SPI + + spi = SPI(1, sck=Pin(12), mosi=Pin(13), miso=Pin(14)) + lan = network.LAN(spi=spi, phy_type=network.PHY_W5500, phy_addr=0, + cs=Pin(10), int=Pin(11)) + +.. note:: WIZnet W5500 Ethernet is also supported on some other MicroPython + ports, but using a :ref:`different software interface + `. + Delay and timing ---------------- @@ -187,8 +271,10 @@ Use the :mod:`time " + + +def callback(*args): + raise ValueError("weakref callback", args) + + +def test(): + print("test ref with exception in the callback") + a = A() + r = weakref.ref(a, callback) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print("collect done") + + print("test finalize with exception in the callback") + a = A() + weakref.finalize(a, callback) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print("collect done") + + +test() diff --git a/tests/basics/weakref_callback_exception.py.exp b/tests/basics/weakref_callback_exception.py.exp new file mode 100644 index 00000000000..c2f7796310c --- /dev/null +++ b/tests/basics/weakref_callback_exception.py.exp @@ -0,0 +1,12 @@ +test ref with exception in the callback +Unhandled exception in weakref callback: +Traceback (most recent call last): + File "\.\+weakref_callback_exception.py", line 21, in callback +ValueError: ('weakref callback', (,)) +collect done +test finalize with exception in the callback +Unhandled exception in weakref callback: +Traceback (most recent call last): + File "\.\+weakref_callback_exception.py", line 21, in callback +ValueError: ('weakref callback', ()) +collect done diff --git a/tests/basics/weakref_callback_exception.py.native.exp b/tests/basics/weakref_callback_exception.py.native.exp new file mode 100644 index 00000000000..a06a35c3b7e --- /dev/null +++ b/tests/basics/weakref_callback_exception.py.native.exp @@ -0,0 +1,8 @@ +test ref with exception in the callback +Unhandled exception in weakref callback: +ValueError: ('weakref callback', (,)) +collect done +test finalize with exception in the callback +Unhandled exception in weakref callback: +ValueError: ('weakref callback', ()) +collect done diff --git a/tests/basics/weakref_finalize_basic.py b/tests/basics/weakref_finalize_basic.py new file mode 100644 index 00000000000..792cffacb13 --- /dev/null +++ b/tests/basics/weakref_finalize_basic.py @@ -0,0 +1,58 @@ +# Test weakref.finalize() functionality that doesn't require gc.collect(). + +try: + import weakref +except ImportError: + print("SKIP") + raise SystemExit + +# Cannot reference non-heap objects. +for value in (None, False, True, Ellipsis, 0, "", ()): + try: + weakref.finalize(value, lambda: None) + except TypeError: + print(value, "TypeError") + + +# Convert (obj, func, args, kwargs) so CPython and MicroPython have a chance to match. +def convert_4_tuple(values): + if values is None: + return None + return (type(values[0]).__name__, type(values[1]), values[2], values[3]) + + +class A: + def __str__(self): + return "" + + +print("test alive, peek, detach") +a = A() +f = weakref.finalize(a, lambda: None, 1, 2, kwarg=3) +print("alive", f.alive) +print("peek", convert_4_tuple(f.peek())) +print("detach", convert_4_tuple(f.detach())) +print("alive", f.alive) +print("peek", convert_4_tuple(f.peek())) +print("detach", convert_4_tuple(f.detach())) +print("call", f()) +a = None + +print("test alive, peek, call") +a = A() +f = weakref.finalize(a, lambda *args, **kwargs: (args, kwargs), 1, 2, kwarg=3) +print("alive", f.alive) +print("peek", convert_4_tuple(f.peek())) +print("call", f()) +print("alive", f.alive) +print("peek", convert_4_tuple(f.peek())) +print("call", f()) +print("detach", convert_4_tuple(f.detach())) + +print("test call which raises exception") +a = A() +f = weakref.finalize(a, lambda: 1 / 0) +try: + f() +except ZeroDivisionError as er: + print("call ZeroDivisionError") diff --git a/tests/basics/weakref_finalize_collect.py b/tests/basics/weakref_finalize_collect.py new file mode 100644 index 00000000000..f6e7c14843e --- /dev/null +++ b/tests/basics/weakref_finalize_collect.py @@ -0,0 +1,76 @@ +# Test weakref.finalize() functionality requiring gc.collect(). +# Should be kept in sync with tests/ports/webassembly/weakref_finalize_collect.py. + +try: + import weakref +except ImportError: + print("SKIP") + raise SystemExit + +# gc module must be available if weakref is. +import gc + + +class A: + def __str__(self): + return "" + + +def callback(*args, **kwargs): + print("callback({}, {})".format(args, kwargs)) + return 42 + + +def test(): + print("test basic use of finalize() with a simple callback") + a = A() + f = weakref.finalize(a, callback) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print("alive", f.alive) + print("peek", f.peek()) + print("detach", f.detach()) + print("call", f()) + + print("test that a callback is passed the correct values") + a = A() + f = weakref.finalize(a, callback, 1, 2, kwarg=3) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print("alive", f.alive) + print("peek", f.peek()) + print("detach", f.detach()) + print("call", f()) + + print("test that calling the finalizer cancels the finalizer") + a = A() + f = weakref.finalize(a, callback) + print(f()) + print(a) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + + print("test that calling detach cancels the finalizer") + a = A() + f = weakref.finalize(a, callback) + print(len(f.detach())) + print(a) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + + print("test that finalize does not get collected before its ref does") + a = A() + weakref.finalize(a, callback) + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print("free a") + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + + +test() diff --git a/tests/basics/weakref_multiple_refs.py b/tests/basics/weakref_multiple_refs.py new file mode 100644 index 00000000000..400e03a17c7 --- /dev/null +++ b/tests/basics/weakref_multiple_refs.py @@ -0,0 +1,36 @@ +# Test weakref when multiple weak references are active. +# +# This test has different output to CPython due to the order that MicroPython +# executes weak reference callbacks. + +try: + import weakref +except ImportError: + print("SKIP") + raise SystemExit + +# gc module must be available if weakref is. +import gc + + +class A: + def __str__(self): + return "" + + +def test(): + global r1, r2 # needed for webassembly port to retain references to them + + print("test having multiple ref and finalize objects referencing the same thing") + a = A() + r1 = weakref.ref(a, lambda r: print("ref1", r())) + f1 = weakref.finalize(a, lambda: print("finalize1")) + r2 = weakref.ref(a, lambda r: print("ref2", r())) + f2 = weakref.finalize(a, lambda: print("finalize2")) + print(r1(), f1.alive, r2(), f2.alive) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + + +test() diff --git a/tests/basics/weakref_multiple_refs.py.exp b/tests/basics/weakref_multiple_refs.py.exp new file mode 100644 index 00000000000..1f2d366f776 --- /dev/null +++ b/tests/basics/weakref_multiple_refs.py.exp @@ -0,0 +1,6 @@ +test having multiple ref and finalize objects referencing the same thing + True True +finalize2 +finalize1 +ref2 None +ref1 None diff --git a/tests/basics/weakref_ref_basic.py b/tests/basics/weakref_ref_basic.py new file mode 100644 index 00000000000..058045f6c33 --- /dev/null +++ b/tests/basics/weakref_ref_basic.py @@ -0,0 +1,14 @@ +# Test weakref.ref() functionality that doesn't require gc.collect(). + +try: + import weakref +except ImportError: + print("SKIP") + raise SystemExit + +# Cannot reference non-heap objects. +for value in (None, False, True, Ellipsis, 0, "", ()): + try: + weakref.ref(value) + except TypeError: + print(value, "TypeError") diff --git a/tests/basics/weakref_ref_collect.py b/tests/basics/weakref_ref_collect.py new file mode 100644 index 00000000000..0e8db977d77 --- /dev/null +++ b/tests/basics/weakref_ref_collect.py @@ -0,0 +1,69 @@ +# Test weakref.ref() functionality requiring gc.collect(). +# Should be kept in sync with tests/ports/webassembly/weakref_ref_collect.py. + +try: + import weakref +except ImportError: + print("SKIP") + raise SystemExit + +# gc module must be available if weakref is. +import gc + +# Cannot reference non-heap objects. +for value in (None, False, True, Ellipsis, 0, "", ()): + try: + weakref.ref(value) + except TypeError: + print(value, "TypeError") + + +class A: + def __str__(self): + return "" + + +def callback(r): + print("callback", r()) + + +def test(): + print("test basic use of ref() with only one argument") + a = A() + r = weakref.ref(a) + print(r()) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print(r()) + + print("test use of ref() with a callback") + a = A() + r = weakref.ref(a, callback) + print(r()) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print(r()) + + print("test when weakref gets collected before the object it refs") + a = A() + r = weakref.ref(a, callback) + print(r()) + r = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + a = None + + print("test a double reference") + a = A() + r1 = weakref.ref(a, callback) + r2 = weakref.ref(a, callback) + print(r1(), r2()) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print(r1(), r2()) + + +test() diff --git a/tests/cmdline/cmd_compile_only.py b/tests/cmdline/cmd_compile_only.py new file mode 100644 index 00000000000..89964c1b5bd --- /dev/null +++ b/tests/cmdline/cmd_compile_only.py @@ -0,0 +1,13 @@ +# cmdline: -X compile-only +# test compile-only functionality +print("This should not be printed") +x = 1 + 2 + + +def hello(): + return "world" + + +class TestClass: + def __init__(self): + self.value = 42 diff --git a/tests/cmdline/cmd_compile_only.py.exp b/tests/cmdline/cmd_compile_only.py.exp new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/tests/cmdline/cmd_compile_only.py.exp @@ -0,0 +1 @@ + diff --git a/tests/cmdline/cmd_compile_only_error.py b/tests/cmdline/cmd_compile_only_error.py new file mode 100644 index 00000000000..326937a5c07 --- /dev/null +++ b/tests/cmdline/cmd_compile_only_error.py @@ -0,0 +1,6 @@ +# cmdline: -X compile-only +# test compile-only with syntax error +print("This should not be printed") +def broken_syntax( + # Missing closing parenthesis + return "error" diff --git a/tests/cmdline/cmd_compile_only_error.py.exp b/tests/cmdline/cmd_compile_only_error.py.exp new file mode 100644 index 00000000000..3911f71d624 --- /dev/null +++ b/tests/cmdline/cmd_compile_only_error.py.exp @@ -0,0 +1 @@ +CRASH \ No newline at end of file diff --git a/tests/cmdline/cmd_file_variable.py b/tests/cmdline/cmd_file_variable.py new file mode 100644 index 00000000000..6cac6744d90 --- /dev/null +++ b/tests/cmdline/cmd_file_variable.py @@ -0,0 +1,5 @@ +# Test that __file__ is set correctly for script execution +try: + print("__file__ =", __file__) +except NameError: + print("__file__ not defined") diff --git a/tests/cmdline/cmd_file_variable.py.exp b/tests/cmdline/cmd_file_variable.py.exp new file mode 100644 index 00000000000..420f789ddf3 --- /dev/null +++ b/tests/cmdline/cmd_file_variable.py.exp @@ -0,0 +1 @@ +__file__ = \.\*cmdline/cmd_file_variable.py diff --git a/tests/cmdline/cmd_module_atexit.py b/tests/cmdline/cmd_module_atexit.py new file mode 100644 index 00000000000..100bc112777 --- /dev/null +++ b/tests/cmdline/cmd_module_atexit.py @@ -0,0 +1,16 @@ +# cmdline: -m cmdline.cmd_module_atexit +# +# Test running as a module and using sys.atexit. + +import sys + +if not hasattr(sys, "atexit"): + print("SKIP") + raise SystemExit + +# Verify we ran as a module. +print(sys.argv) + +sys.atexit(lambda: print("done")) + +print("start") diff --git a/tests/cmdline/cmd_module_atexit.py.exp b/tests/cmdline/cmd_module_atexit.py.exp new file mode 100644 index 00000000000..334bf53bac9 --- /dev/null +++ b/tests/cmdline/cmd_module_atexit.py.exp @@ -0,0 +1,3 @@ +['cmdline.cmd_module_atexit', '\.\*cmdline/cmd_module_atexit.py'] +start +done diff --git a/tests/cmdline/cmd_module_atexit_exc.py b/tests/cmdline/cmd_module_atexit_exc.py new file mode 100644 index 00000000000..88940a7741f --- /dev/null +++ b/tests/cmdline/cmd_module_atexit_exc.py @@ -0,0 +1,19 @@ +# cmdline: -m cmdline.cmd_module_atexit_exc +# +# Test running as a module and using sys.atexit, with script completion via sys.exit. + +import sys + +if not hasattr(sys, "atexit"): + print("SKIP") + raise SystemExit + +# Verify we ran as a module. +print(sys.argv) + +sys.atexit(lambda: print("done")) + +print("start") + +# This will raise SystemExit to finish the script, and atexit should still be run. +sys.exit(0) diff --git a/tests/cmdline/cmd_module_atexit_exc.py.exp b/tests/cmdline/cmd_module_atexit_exc.py.exp new file mode 100644 index 00000000000..210748afdfe --- /dev/null +++ b/tests/cmdline/cmd_module_atexit_exc.py.exp @@ -0,0 +1,3 @@ +['cmdline.cmd_module_atexit_exc', '\.\*cmdline/cmd_module_atexit_exc.py'] +start +done diff --git a/tests/cmdline/cmd_showbc.py.exp b/tests/cmdline/cmd_showbc.py.exp index db06de92371..b839de6d1ed 100644 --- a/tests/cmdline/cmd_showbc.py.exp +++ b/tests/cmdline/cmd_showbc.py.exp @@ -1,4 +1,4 @@ -File cmdline/cmd_showbc.py, code block '' (descriptor: \.\+, bytecode @\.\+ 63 bytes) +File \.\*cmdline/cmd_showbc.py, code block '' (descriptor: \.\+, bytecode @\.\+ 63 bytes) Raw bytecode (code_info_size=18, bytecode_size=45): 10 20 01 60 20 84 7d 64 60 88 07 64 60 69 20 62 64 20 32 00 16 05 32 01 16 05 81 2a 01 53 33 02 @@ -47,7 +47,7 @@ arg names: 42 IMPORT_STAR 43 LOAD_CONST_NONE 44 RETURN_VALUE -File cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 46\[68\] bytes) +File \.\*cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 46\[68\] bytes) Raw bytecode (code_info_size=8\[46\], bytecode_size=382): a8 12 9\[bf\] 03 05 60 60 26 22 24 64 22 24 25 25 24 26 23 63 22 22 25 23 23 2f 6c 25 65 25 25 69 68 @@ -185,7 +185,7 @@ arg names: 58 UNARY_OP 1 __neg__ 59 STORE_FAST 9 60 LOAD_FAST 0 -61 UNARY_OP 3 +61 UNARY_OP 3 \$ 62 STORE_FAST 10 63 LOAD_FAST 0 64 LOAD_DEREF 14 @@ -206,7 +206,7 @@ arg names: 84 LOAD_DEREF 14 86 LOAD_FAST 1 87 BINARY_OP 2 __eq__ -88 UNARY_OP 3 +88 UNARY_OP 3 \$ 89 STORE_FAST 10 90 LOAD_DEREF 14 92 LOAD_ATTR c @@ -411,7 +411,7 @@ arg names: 379 RETURN_VALUE 380 LOAD_CONST_NONE 381 RETURN_VALUE -File cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 59 bytes) +File \.\*cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 59 bytes) Raw bytecode (code_info_size=8, bytecode_size=51): a8 10 0a 05 80 82 34 38 81 57 c0 57 c1 57 c2 57 c3 57 c4 57 c5 57 c6 57 c7 57 c8 c9 82 57 ca 57 @@ -470,7 +470,7 @@ arg names: 48 POP_TOP 49 LOAD_CONST_NONE 50 RETURN_VALUE -File cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 20 bytes) +File \.\*cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 20 bytes) Raw bytecode (code_info_size=9, bytecode_size=11): a1 01 0b 05 06 80 88 40 00 82 2a 01 53 b0 21 00 01 c1 51 63 @@ -489,7 +489,7 @@ arg names: a 08 STORE_FAST 1 09 LOAD_CONST_NONE 10 RETURN_VALUE -File cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 21 bytes) +File \.\*cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 21 bytes) Raw bytecode (code_info_size=8, bytecode_size=13): 88 40 0a 05 80 8f 23 23 51 67 59 81 67 59 81 5e 51 68 59 51 63 @@ -513,7 +513,7 @@ arg names: 10 POP_TOP 11 LOAD_CONST_NONE 12 RETURN_VALUE -File cmdline/cmd_showbc.py, code block 'Class' (descriptor: \.\+, bytecode @\.\+ 1\[56\] bytes) +File \.\*cmdline/cmd_showbc.py, code block 'Class' (descriptor: \.\+, bytecode @\.\+ 1\[56\] bytes) Raw bytecode (code_info_size=\[56\], bytecode_size=10): 00 \.\+ 11 0f 16 10 10 02 16 11 51 63 arg names: @@ -528,7 +528,7 @@ arg names: 06 STORE_NAME __qualname__ 08 LOAD_CONST_NONE 09 RETURN_VALUE -File cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 18 bytes) +File \.\*cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 18 bytes) Raw bytecode (code_info_size=6, bytecode_size=12): 19 08 05 12 80 9c 12 13 12 14 b0 15 05 36 00 59 51 63 @@ -545,7 +545,7 @@ arg names: self 09 POP_TOP 10 LOAD_CONST_NONE 11 RETURN_VALUE -File cmdline/cmd_showbc.py, code block '' (descriptor: \.\+, bytecode @\.\+ 28 bytes) +File \.\*cmdline/cmd_showbc.py, code block '' (descriptor: \.\+, bytecode @\.\+ 28 bytes) Raw bytecode (code_info_size=9, bytecode_size=19): c3 40 0c 09 03 03 03 80 3b 53 b2 53 53 4b 0b c3 25 01 44 39 25 00 67 59 42 33 51 63 @@ -568,7 +568,7 @@ arg names: * * * 15 JUMP 4 17 LOAD_CONST_NONE 18 RETURN_VALUE -File cmdline/cmd_showbc.py, code block '' (descriptor: \.\+, bytecode @\.\+ 26 bytes) +File \.\*cmdline/cmd_showbc.py, code block '' (descriptor: \.\+, bytecode @\.\+ 26 bytes) Raw bytecode (code_info_size=8, bytecode_size=18): 4b 0c 0a 03 03 03 80 3c 2b 00 b2 5f 4b 0b c3 25 01 44 39 25 00 2f 14 42 33 63 @@ -588,7 +588,7 @@ arg names: * * * 13 STORE_COMP 20 15 JUMP 4 17 RETURN_VALUE -File cmdline/cmd_showbc.py, code block '' (descriptor: \.\+, bytecode @\.\+ 28 bytes) +File \.\*cmdline/cmd_showbc.py, code block '' (descriptor: \.\+, bytecode @\.\+ 28 bytes) Raw bytecode (code_info_size=8, bytecode_size=20): 53 0c 0b 03 03 03 80 3d 2c 00 b2 5f 4b 0d c3 25 01 44 39 25 00 25 00 2f 19 42 31 63 @@ -609,7 +609,7 @@ arg names: * * * 15 STORE_COMP 25 17 JUMP 4 19 RETURN_VALUE -File cmdline/cmd_showbc.py, code block 'closure' (descriptor: \.\+, bytecode @\.\+ 20 bytes) +File \.\*cmdline/cmd_showbc.py, code block 'closure' (descriptor: \.\+, bytecode @\.\+ 20 bytes) Raw bytecode (code_info_size=8, bytecode_size=12): 19 0c 0c 03 80 6f 25 23 25 00 81 f2 c1 81 27 00 29 00 51 63 @@ -629,7 +629,7 @@ arg names: * 08 DELETE_DEREF 0 10 LOAD_CONST_NONE 11 RETURN_VALUE -File cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 13 bytes) +File \.\*cmdline/cmd_showbc.py, code block 'f' (descriptor: \.\+, bytecode @\.\+ 13 bytes) Raw bytecode (code_info_size=8, bytecode_size=5): 9a 01 0a 05 03 08 80 8b b1 25 00 f2 63 arg names: * b diff --git a/tests/cmdline/cmd_showbc_const.py.exp b/tests/cmdline/cmd_showbc_const.py.exp index 6cdc3e9c963..6faeb941d7d 100644 --- a/tests/cmdline/cmd_showbc_const.py.exp +++ b/tests/cmdline/cmd_showbc_const.py.exp @@ -1,4 +1,4 @@ -File cmdline/cmd_showbc_const.py, code block '' (descriptor: \.\+, bytecode @\.\+ 198 bytes) +File \.\*cmdline/cmd_showbc_const.py, code block '' (descriptor: \.\+, bytecode @\.\+ 198 bytes) Raw bytecode (code_info_size=40, bytecode_size=158): 2c 4c 01 60 2c 46 22 65 27 4a 83 0c 20 27 40 20 27 20 27 40 60 20 27 24 40 60 40 24 27 47 24 27 @@ -76,7 +76,7 @@ arg names: 34 RAISE_OBJ 35 DUP_TOP 36 LOAD_NAME AttributeError -38 BINARY_OP 8 +38 BINARY_OP 8 \$ 39 POP_JUMP_IF_FALSE 44 41 POP_TOP 42 POP_EXCEPT_JUMP 45 diff --git a/tests/cmdline/cmd_showbc_opt.py.exp b/tests/cmdline/cmd_showbc_opt.py.exp index 9e4e4fae10c..c3047275a9c 100644 --- a/tests/cmdline/cmd_showbc_opt.py.exp +++ b/tests/cmdline/cmd_showbc_opt.py.exp @@ -1,4 +1,4 @@ -File cmdline/cmd_showbc_opt.py, code block '' (descriptor: \.\+, bytecode @\.\+ 35 bytes) +File \.\*cmdline/cmd_showbc_opt.py, code block '' (descriptor: \.\+, bytecode @\.\+ 35 bytes) Raw bytecode (code_info_size=13, bytecode_size=22): 00 16 01 60 20 64 40 84 07 64 40 84 07 32 00 16 02 32 01 16 03 32 02 16 04 32 03 16 05 32 04 16 @@ -27,7 +27,7 @@ arg names: 18 STORE_NAME f4 20 LOAD_CONST_NONE 21 RETURN_VALUE -File cmdline/cmd_showbc_opt.py, code block 'f0' (descriptor: \.\+, bytecode @\.\+ 8 bytes) +File \.\*cmdline/cmd_showbc_opt.py, code block 'f0' (descriptor: \.\+, bytecode @\.\+ 8 bytes) Raw bytecode (code_info_size=6, bytecode_size=2): 08 08 02 60 40 22 80 63 arg names: @@ -39,7 +39,7 @@ arg names: bc=2 line=7 00 LOAD_CONST_SMALL_INT 0 01 RETURN_VALUE -File cmdline/cmd_showbc_opt.py, code block 'f1' (descriptor: \.\+, bytecode @\.\+ 22 bytes) +File \.\*cmdline/cmd_showbc_opt.py, code block 'f1' (descriptor: \.\+, bytecode @\.\+ 22 bytes) Raw bytecode (code_info_size=9, bytecode_size=13): 11 0e 03 08 80 0a 23 22 20 b0 44 42 51 63 12 07 82 34 01 59 51 63 @@ -61,7 +61,7 @@ arg names: x 10 POP_TOP 11 LOAD_CONST_NONE 12 RETURN_VALUE -File cmdline/cmd_showbc_opt.py, code block 'f2' (descriptor: \.\+, bytecode @\.\+ 10 bytes) +File \.\*cmdline/cmd_showbc_opt.py, code block 'f2' (descriptor: \.\+, bytecode @\.\+ 10 bytes) Raw bytecode (code_info_size=7, bytecode_size=3): 11 0a 04 08 80 11 23 12 09 65 arg names: x @@ -72,7 +72,7 @@ arg names: x bc=3 line=19 00 LOAD_GLOBAL Exception 02 RAISE_OBJ -File cmdline/cmd_showbc_opt.py, code block 'f3' (descriptor: \.\+, bytecode @\.\+ 24 bytes) +File \.\*cmdline/cmd_showbc_opt.py, code block 'f3' (descriptor: \.\+, bytecode @\.\+ 24 bytes) Raw bytecode (code_info_size=9, bytecode_size=15): 11 0e 05 08 80 16 22 22 23 42 42 42 43 b0 43 3b 12 07 82 34 01 59 51 63 @@ -94,7 +94,7 @@ arg names: x 12 POP_TOP 13 LOAD_CONST_NONE 14 RETURN_VALUE -File cmdline/cmd_showbc_opt.py, code block 'f4' (descriptor: \.\+, bytecode @\.\+ 24 bytes) +File \.\*cmdline/cmd_showbc_opt.py, code block 'f4' (descriptor: \.\+, bytecode @\.\+ 24 bytes) Raw bytecode (code_info_size=9, bytecode_size=15): 11 0e 06 08 80 1d 22 22 23 42 42 42 40 b0 43 3b 12 07 82 34 01 59 51 63 diff --git a/tests/cmdline/cmd_sys_exit_0.py b/tests/cmdline/cmd_sys_exit_0.py new file mode 100644 index 00000000000..1294b739e8f --- /dev/null +++ b/tests/cmdline/cmd_sys_exit_0.py @@ -0,0 +1,5 @@ +# cmdline: +# test sys.exit(0) - success exit code +import sys + +sys.exit(0) diff --git a/tests/cmdline/cmd_sys_exit_0.py.exp b/tests/cmdline/cmd_sys_exit_0.py.exp new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/tests/cmdline/cmd_sys_exit_0.py.exp @@ -0,0 +1 @@ + diff --git a/tests/cmdline/cmd_sys_exit_error.py b/tests/cmdline/cmd_sys_exit_error.py new file mode 100644 index 00000000000..ecac15e94f1 --- /dev/null +++ b/tests/cmdline/cmd_sys_exit_error.py @@ -0,0 +1,5 @@ +# cmdline: +# test sys.exit() functionality and exit codes +import sys + +sys.exit(123) diff --git a/tests/cmdline/cmd_sys_exit_error.py.exp b/tests/cmdline/cmd_sys_exit_error.py.exp new file mode 100644 index 00000000000..3911f71d624 --- /dev/null +++ b/tests/cmdline/cmd_sys_exit_error.py.exp @@ -0,0 +1 @@ +CRASH \ No newline at end of file diff --git a/tests/cmdline/cmd_sys_exit_none.py b/tests/cmdline/cmd_sys_exit_none.py new file mode 100644 index 00000000000..66e19666589 --- /dev/null +++ b/tests/cmdline/cmd_sys_exit_none.py @@ -0,0 +1,5 @@ +# cmdline: +# test sys.exit(None) - should exit with code 0 +import sys + +sys.exit(None) diff --git a/tests/cmdline/cmd_sys_exit_none.py.exp b/tests/cmdline/cmd_sys_exit_none.py.exp new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/tests/cmdline/cmd_sys_exit_none.py.exp @@ -0,0 +1 @@ + diff --git a/tests/cmdline/cmd_verbose.py.exp b/tests/cmdline/cmd_verbose.py.exp index ae833dbec8d..1982f313cb7 100644 --- a/tests/cmdline/cmd_verbose.py.exp +++ b/tests/cmdline/cmd_verbose.py.exp @@ -1,4 +1,4 @@ -File cmdline/cmd_verbose.py, code block '' (descriptor: \.\+, bytecode @\.\+ 12 bytes) +File \.\*cmdline/cmd_verbose.py, code block '' (descriptor: \.\+, bytecode @\.\+ 12 bytes) Raw bytecode (code_info_size=4, bytecode_size=8): 08 04 01 40 11 02 81 34 01 59 51 63 arg names: diff --git a/tests/cmdline/repl_autocomplete.py.exp b/tests/cmdline/repl_autocomplete.py.exp index 75002985e3c..56b754a31f4 100644 --- a/tests/cmdline/repl_autocomplete.py.exp +++ b/tests/cmdline/repl_autocomplete.py.exp @@ -1,5 +1,5 @@ MicroPython \.\+ version -Use \.\+ +Type "help()" for more information. >>> # tests for autocompletion >>> import sys >>> not_exist. @@ -11,4 +11,4 @@ Use \.\+ >>> i.lower('ABC') 'abc' >>> None. ->>> +>>> \$ diff --git a/tests/cmdline/repl_autocomplete_underscore.py b/tests/cmdline/repl_autocomplete_underscore.py new file mode 100644 index 00000000000..a0ad4aadf0f --- /dev/null +++ b/tests/cmdline/repl_autocomplete_underscore.py @@ -0,0 +1,32 @@ +# Test REPL autocompletion filtering of underscore attributes + +# Start paste mode +{\x05} +class TestClass: + def __init__(self): + self.public_attr = 1 + self._private_attr = 2 + self.__very_private = 3 + + def public_method(self): + pass + + def _private_method(self): + pass + + @property + def public_property(self): + return 42 + + @property + def _private_property(self): + return 99 +{\x04} +# Paste executed + +# Create an instance +obj = TestClass() + +# Test tab completion on the instance +# The tab character after `obj.` and 'a' below triggers the completions +obj.{\x09}{\x09}a{\x09} diff --git a/tests/cmdline/repl_autocomplete_underscore.py.exp b/tests/cmdline/repl_autocomplete_underscore.py.exp new file mode 100644 index 00000000000..54882267d9b --- /dev/null +++ b/tests/cmdline/repl_autocomplete_underscore.py.exp @@ -0,0 +1,41 @@ +MicroPython \.\+ version +Type "help()" for more information. +>>> # Test REPL autocompletion filtering of underscore attributes +>>> \$ +>>> # Start paste mode +>>> \$ +paste mode; Ctrl-C to cancel, Ctrl-D to finish +=== \$ +=== class TestClass: +=== def __init__(self): +=== self.public_attr = 1 +=== self._private_attr = 2 +=== self.__very_private = 3 +=== \$ +=== def public_method(self): +=== pass +=== \$ +=== def _private_method(self): +=== pass +=== \$ +=== @property +=== def public_property(self): +=== return 42 +=== \$ +=== @property +=== def _private_property(self): +=== return 99 +=== \$ +>>> \$ +>>> # Paste executed +>>> \$ +>>> # Create an instance +>>> obj = TestClass() +>>> \$ +>>> # Test tab completion on the instance +>>> # The tab character after `obj.` and 'a' below triggers the completions +>>> obj.public_ +public_attr public_method public_property +>>> obj.public_attr +1 +>>> \$ diff --git a/tests/cmdline/repl_autoindent.py.exp b/tests/cmdline/repl_autoindent.py.exp index 9127a7d31d9..616ebe89989 100644 --- a/tests/cmdline/repl_autoindent.py.exp +++ b/tests/cmdline/repl_autoindent.py.exp @@ -1,22 +1,22 @@ MicroPython \.\+ version -Use \.\+ +Type "help()" for more information. >>> # tests for autoindent >>> if 1: ... print(1) -... -... -... +... \$ +... \$ +... \$ 1 >>> if 0: ...  print(2) ... else: ... print(3) -... +... \$ 3 >>> if 0: ... print(4) ... else: ... print(5) -... +... \$ 5 ->>> +>>> \$ diff --git a/tests/cmdline/repl_basic.py.exp b/tests/cmdline/repl_basic.py.exp index 2b390ea98bb..ea013f641a5 100644 --- a/tests/cmdline/repl_basic.py.exp +++ b/tests/cmdline/repl_basic.py.exp @@ -1,5 +1,5 @@ MicroPython \.\+ version -Use \.\+ +Type "help()" for more information. >>> # basic REPL tests >>> print(1) 1 @@ -7,4 +7,4 @@ Use \.\+ 1 >>> 2 2 ->>> +>>> \$ diff --git a/tests/cmdline/repl_cont.py.exp b/tests/cmdline/repl_cont.py.exp index 834c18a4d36..1083eedc73e 100644 --- a/tests/cmdline/repl_cont.py.exp +++ b/tests/cmdline/repl_cont.py.exp @@ -1,5 +1,5 @@ MicroPython \.\+ version -Use \.\+ +Type "help()" for more information. >>> # check REPL allows to continue input >>> 1 \\\\ ... + 2 @@ -54,4 +54,4 @@ two >>> if1 = 2 >>> print(if1) 2 ->>> +>>> \$ diff --git a/tests/cmdline/repl_ctrl_c_interrupt_execution.py b/tests/cmdline/repl_ctrl_c_interrupt_execution.py new file mode 100644 index 00000000000..b125e2c16b4 --- /dev/null +++ b/tests/cmdline/repl_ctrl_c_interrupt_execution.py @@ -0,0 +1,8 @@ +# sigint: deliver via controlling terminal +# Test that Ctrl-C (SIGINT) interrupts blocking code execution. +# MicroPython restores original terminal mode (ISIG on) during +# execution, so the PTY terminal driver generates SIGINT from \x03. +import time +time.sleep(10) +{\x03} +print('repl still responds') diff --git a/tests/cmdline/repl_ctrl_c_interrupt_execution.py.exp b/tests/cmdline/repl_ctrl_c_interrupt_execution.py.exp new file mode 100644 index 00000000000..48baad0193a --- /dev/null +++ b/tests/cmdline/repl_ctrl_c_interrupt_execution.py.exp @@ -0,0 +1,15 @@ +MicroPython \.\+ version +Type "help()" for more information. +>>> # sigint: deliver via controlling terminal +>>> # Test that Ctrl-C (SIGINT) interrupts blocking code execution. +>>> # MicroPython restores original terminal mode (ISIG on) during +>>> # execution, so the PTY terminal driver generates SIGINT from \x03. +>>> import time +>>> time.sleep(10) +######## +\.\*Traceback (most recent call last): + File "", line 1, in +KeyboardInterrupt: \$ +>>> print('repl still responds') +repl still responds +>>> \$ diff --git a/tests/cmdline/repl_emacs_keys.py.exp b/tests/cmdline/repl_emacs_keys.py.exp index 6102c19639a..52d6ac8bab0 100644 --- a/tests/cmdline/repl_emacs_keys.py.exp +++ b/tests/cmdline/repl_emacs_keys.py.exp @@ -1,5 +1,5 @@ MicroPython \.\+ version -Use \.\+ +Type "help()" for more information. >>> # REPL tests of GNU-ish readline navigation >>> # history buffer navigation >>> 1 @@ -16,4 +16,4 @@ Use \.\+ >>> t = 121 >>> \.\+ 'foobar' ->>> +>>> \$ diff --git a/tests/cmdline/repl_inspect.py.exp b/tests/cmdline/repl_inspect.py.exp index 051acfd153a..66258a9faad 100644 --- a/tests/cmdline/repl_inspect.py.exp +++ b/tests/cmdline/repl_inspect.py.exp @@ -1,6 +1,6 @@ test MicroPython \.\+ version -Use \.\+ +Type "help()" for more information. >>> # cmdline: -i -c print("test") >>> # -c option combined with -i option results in REPL ->>> +>>> \$ diff --git a/tests/cmdline/repl_lock.py b/tests/cmdline/repl_lock.py new file mode 100644 index 00000000000..77e2e932740 --- /dev/null +++ b/tests/cmdline/repl_lock.py @@ -0,0 +1,7 @@ +import micropython +micropython.heap_lock() +1+1 +micropython.heap_lock() +None # Cause the repl's line storage to be enlarged ---------------- +micropython.heap_lock() +raise SystemExit diff --git a/tests/cmdline/repl_lock.py.exp b/tests/cmdline/repl_lock.py.exp new file mode 100644 index 00000000000..d921fd6ae8e --- /dev/null +++ b/tests/cmdline/repl_lock.py.exp @@ -0,0 +1,10 @@ +MicroPython \.\+ version +Type "help()" for more information. +>>> import micropython +>>> micropython.heap_lock() +>>> 1+1 +2 +>>> micropython.heap_lock() +>>> None # Cause the repl's line storage to be enlarged ---------------- +>>> micropython.heap_lock() +>>> raise SystemExit diff --git a/tests/cmdline/repl_micropyinspect.py.exp b/tests/cmdline/repl_micropyinspect.py.exp index 93ff43546ea..2789df3f857 100644 --- a/tests/cmdline/repl_micropyinspect.py.exp +++ b/tests/cmdline/repl_micropyinspect.py.exp @@ -1,5 +1,5 @@ MicroPython \.\+ version -Use \.\+ +Type "help()" for more information. >>> # cmdline: cmdline/repl_micropyinspect >>> # setting MICROPYINSPECT environment variable before program exit triggers REPL ->>> +>>> \$ diff --git a/tests/cmdline/repl_paste.py b/tests/cmdline/repl_paste.py new file mode 100644 index 00000000000..8c4f341701f --- /dev/null +++ b/tests/cmdline/repl_paste.py @@ -0,0 +1,90 @@ +# Test REPL paste mode functionality + +# Basic paste mode with a simple function +{\x05} +def hello(): + print('Hello from paste mode!') +hello() +{\x04} + +# Paste mode with multiple indentation levels +{\x05} +def calculate(n): + if n > 0: + for i in range(n): + if i % 2 == 0: + print(f'Even: {i}') + else: + print(f'Odd: {i}') + else: + print('n must be positive') + +calculate(5) +{\x04} + +# Paste mode with blank lines +{\x05} +def function_with_blanks(): + print('First line') +{\x20}{\x20}{\x20}{\x20} + print('After blank line') +{\x20}{\x20}{\x20}{\x20} +{\x20}{\x20}{\x20}{\x20} + print('After two blank lines') + +function_with_blanks() +{\x04} + +# Paste mode with class definition and multiple methods +{\x05} +class TestClass: + def __init__(self, value): + self.value = value +{\x20}{\x20}{\x20}{\x20} + def display(self): + print(f'Value is: {self.value}') +{\x20}{\x20}{\x20}{\x20} + def double(self): + self.value *= 2 + return self.value + +obj = TestClass(21) +obj.display() +print(f'Doubled: {obj.double()}') +obj.display() +{\x04} + +# Paste mode with exception handling +{\x05} +try: + x = 1 / 0 +except ZeroDivisionError: + print('Caught division by zero') +finally: + print('Finally block executed') +{\x04} + +# Cancel paste mode with Ctrl-C +{\x05} +print('This should not execute') +{\x03} + +# Normal REPL still works after cancelled paste +print('Back to normal REPL') + +# Paste mode with syntax error +{\x05} +def bad_syntax(: + print('Missing parameter') +{\x04} + +# Paste mode with runtime error +{\x05} +def will_error(): + undefined_variable +{\x20}{\x20}{\x20}{\x20} +will_error() +{\x04} + +# Final test to show REPL is still functioning +1 + 2 + 3 diff --git a/tests/cmdline/repl_paste.py.exp b/tests/cmdline/repl_paste.py.exp new file mode 100644 index 00000000000..a95d9fd2f7b --- /dev/null +++ b/tests/cmdline/repl_paste.py.exp @@ -0,0 +1,140 @@ +MicroPython \.\+ version +Type "help()" for more information. +>>> # Test REPL paste mode functionality +>>> \$ +>>> # Basic paste mode with a simple function +>>> \$ +paste mode; Ctrl-C to cancel, Ctrl-D to finish +=== \$ +=== def hello(): +=== print('Hello from paste mode!') +=== hello() +=== \$ +Hello from paste mode! +>>> \$ +>>> \$ +>>> # Paste mode with multiple indentation levels +>>> \$ +paste mode; Ctrl-C to cancel, Ctrl-D to finish +=== \$ +=== def calculate(n): +=== if n > 0: +=== for i in range(n): +=== if i % 2 == 0: +=== print(f'Even: {i}') +=== else: +=== print(f'Odd: {i}') +=== else: +=== print('n must be positive') +=== \$ +=== calculate(5) +=== \$ +Even: 0 +Odd: 1 +Even: 2 +Odd: 3 +Even: 4 +>>> \$ +>>> \$ +>>> # Paste mode with blank lines +>>> \$ +paste mode; Ctrl-C to cancel, Ctrl-D to finish +=== \$ +=== def function_with_blanks(): +=== print('First line') +=== \$ +=== print('After blank line') +=== \$ +=== \$ +=== print('After two blank lines') +=== \$ +=== function_with_blanks() +=== \$ +First line +After blank line +After two blank lines +>>> \$ +>>> \$ +>>> # Paste mode with class definition and multiple methods +>>> \$ +paste mode; Ctrl-C to cancel, Ctrl-D to finish +=== \$ +=== class TestClass: +=== def __init__(self, value): +=== self.value = value +=== \$ +=== def display(self): +=== print(f'Value is: {self.value}') +=== \$ +=== def double(self): +=== self.value *= 2 +=== return self.value +=== \$ +=== obj = TestClass(21) +=== obj.display() +=== print(f'Doubled: {obj.double()}') +=== obj.display() +=== \$ +Value is: 21 +Doubled: 42 +Value is: 42 +>>> \$ +>>> \$ +>>> # Paste mode with exception handling +>>> \$ +paste mode; Ctrl-C to cancel, Ctrl-D to finish +=== \$ +=== try: +=== x = 1 / 0 +=== except ZeroDivisionError: +=== print('Caught division by zero') +=== finally: +=== print('Finally block executed') +=== \$ +Caught division by zero +Finally block executed +>>> \$ +>>> \$ +>>> # Cancel paste mode with Ctrl-C +>>> \$ +paste mode; Ctrl-C to cancel, Ctrl-D to finish +=== \$ +=== print('This should not execute') +=== \$ +>>> \$ +>>> \$ +>>> # Normal REPL still works after cancelled paste +>>> print('Back to normal REPL') +Back to normal REPL +>>> \$ +>>> # Paste mode with syntax error +>>> \$ +paste mode; Ctrl-C to cancel, Ctrl-D to finish +=== \$ +=== def bad_syntax(: +=== print('Missing parameter') +=== \$ +Traceback (most recent call last): + File "", line 2 +SyntaxError: invalid syntax +>>> \$ +>>> \$ +>>> # Paste mode with runtime error +>>> \$ +paste mode; Ctrl-C to cancel, Ctrl-D to finish +=== \$ +=== def will_error(): +=== undefined_variable +=== \$ +=== will_error() +=== \$ +Traceback (most recent call last): + File "", line 5, in + File "", line 3, in will_error +NameError: name 'undefined_variable' isn't defined +>>> \$ +>>> \$ +>>> # Final test to show REPL is still functioning +>>> 1 + 2 + 3 +6 +>>> \$ diff --git a/tests/cmdline/repl_sys_ps1_ps2.py.exp b/tests/cmdline/repl_sys_ps1_ps2.py.exp index 9e82db5e313..6781660bf33 100644 --- a/tests/cmdline/repl_sys_ps1_ps2.py.exp +++ b/tests/cmdline/repl_sys_ps1_ps2.py.exp @@ -1,5 +1,5 @@ MicroPython \.\+ version -Use \.\+ +Type "help()" for more information. >>> # test changing ps1/ps2 >>> import sys >>> sys.ps1 = "PS1" diff --git a/tests/cmdline/repl_words_move.py.exp b/tests/cmdline/repl_words_move.py.exp index 86f6b778898..64d1e13d3e9 100644 --- a/tests/cmdline/repl_words_move.py.exp +++ b/tests/cmdline/repl_words_move.py.exp @@ -1,5 +1,5 @@ MicroPython \.\+ version -Use \.\+ +Type "help()" for more information. >>> # word movement >>> # backward-word, start in word >>> \.\+ @@ -19,7 +19,7 @@ Use \.\+ >>> # forward-word on eol. if cursor is moved, this will result in a SyntaxError >>> \.\+ 6 ->>> +>>> \$ >>> # kill word >>> # backward-kill-word, start in word >>> \.\+ @@ -33,7 +33,7 @@ Use \.\+ >>> # forward-kill-word, don't start in word >>> \.\+ 3 ->>> +>>> \$ >>> # extra move/kill shortcuts >>> # ctrl-left >>> \.\+ @@ -44,4 +44,4 @@ Use \.\+ >>> # ctrl-w >>> \.\+ 1 ->>> +>>> \$ diff --git a/tests/cpydiff/builtin_next_arg2.py b/tests/cpydiff/builtin_next_arg2.py deleted file mode 100644 index 5df2d6e70f8..00000000000 --- a/tests/cpydiff/builtin_next_arg2.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -categories: Modules,builtins -description: Second argument to next() is not implemented -cause: MicroPython is optimised for code space. -workaround: Instead of ``val = next(it, deflt)`` use:: - - try: - val = next(it) - except StopIteration: - val = deflt -""" -print(next(iter(range(0)), 42)) diff --git a/tests/cpydiff/core_class_delnotimpl.py b/tests/cpydiff/core_class_delnotimpl.py index 18c176e9bb1..6fac764ac98 100644 --- a/tests/cpydiff/core_class_delnotimpl.py +++ b/tests/cpydiff/core_class_delnotimpl.py @@ -4,6 +4,7 @@ cause: Unknown workaround: Unknown """ + import gc diff --git a/tests/cpydiff/core_class_dir.py b/tests/cpydiff/core_class_dir.py new file mode 100644 index 00000000000..e2b0e877cdc --- /dev/null +++ b/tests/cpydiff/core_class_dir.py @@ -0,0 +1,14 @@ +""" +categories: Core,Classes +description: dir() does not convert __dir__ return value to a sorted list +cause: MicroPython's dir() returns the value from __dir__ as-is, without iterating it into a list or sorting it. +workaround: Have __dir__ return a sorted list directly. +""" + + +class C: + def __dir__(self): + return "cba" + + +print(dir(C())) diff --git a/tests/cpydiff/core_class_initsubclass.py b/tests/cpydiff/core_class_initsubclass.py new file mode 100644 index 00000000000..8683271dcb0 --- /dev/null +++ b/tests/cpydiff/core_class_initsubclass.py @@ -0,0 +1,21 @@ +""" +categories: Core,Classes +description: ``__init_subclass__`` isn't automatically called. +cause: MicroPython does not currently implement PEP 487. +workaround: Manually call ``__init_subclass__`` after class creation if needed. e.g.:: + + class A(Base): + pass + A.__init_subclass__() + +""" + + +class Base: + @classmethod + def __init_subclass__(cls): + print(f"Base.__init_subclass__({cls.__name__})") + + +class A(Base): + pass diff --git a/tests/cpydiff/core_class_initsubclass_autoclassmethod.py b/tests/cpydiff/core_class_initsubclass_autoclassmethod.py new file mode 100644 index 00000000000..b2f7628976c --- /dev/null +++ b/tests/cpydiff/core_class_initsubclass_autoclassmethod.py @@ -0,0 +1,31 @@ +""" +categories: Core,Classes +description: ``__init_subclass__`` isn't an implicit classmethod. +cause: MicroPython does not currently implement PEP 487. ``__init_subclass__`` is not currently in the list of special-cased class/static methods. +workaround: Decorate declarations of ``__init_subclass__`` with ``@classmethod``. +""" + + +def regularize_spelling(text, prefix="bound_"): + # for regularizing across the CPython "method" vs MicroPython "bound_method" spelling for the type of a bound classmethod + if text.startswith(prefix): + return text[len(prefix) :] + return text + + +class A: + def __init_subclass__(cls): + pass + + @classmethod + def manual_decorated(cls): + pass + + +a = type(A.__init_subclass__).__name__ +b = type(A.manual_decorated).__name__ + +print(regularize_spelling(a)) +print(regularize_spelling(b)) +if a != b: + print("FAIL") diff --git a/tests/cpydiff/core_class_initsubclass_kwargs.py b/tests/cpydiff/core_class_initsubclass_kwargs.py new file mode 100644 index 00000000000..ed5157afeae --- /dev/null +++ b/tests/cpydiff/core_class_initsubclass_kwargs.py @@ -0,0 +1,22 @@ +""" +categories: Core,Classes +description: MicroPython doesn't support parameterized ``__init_subclass__`` class customization. +cause: MicroPython does not currently implement PEP 487. The MicroPython syntax tree does not include a kwargs node after the class inheritance list. +workaround: Use class variables or another mechanism to specify base-class customizations. +""" + + +class Base: + @classmethod + def __init_subclass__(cls, arg=None, **kwargs): + cls.init_subclass_was_called = True + print(f"Base.__init_subclass__({cls.__name__}, {arg=!r}, {kwargs=!r})") + + +class A(Base, arg="arg"): + pass + + +# Regularize across MicroPython not automatically calling __init_subclass__ either. +if not getattr(A, "init_subclass_was_called", False): + A.__init_subclass__() diff --git a/tests/cpydiff/core_class_initsubclass_super.py b/tests/cpydiff/core_class_initsubclass_super.py new file mode 100644 index 00000000000..d18671a74bf --- /dev/null +++ b/tests/cpydiff/core_class_initsubclass_super.py @@ -0,0 +1,22 @@ +""" +categories: Core,Classes +description: ``__init_subclass__`` can't be defined a cooperatively-recursive way. +cause: MicroPython does not currently implement PEP 487. The base object type does not have an ``__init_subclass__`` implementation. +workaround: Omit the recursive ``__init_subclass__`` call unless it's known that the grandparent also defines it. +""" + + +class Base: + @classmethod + def __init_subclass__(cls, **kwargs): + cls.init_subclass_was_called = True + super().__init_subclass__(**kwargs) + + +class A(Base): + pass + + +# Regularize across MicroPython not automatically calling __init_subclass__ either. +if not getattr(A, "init_subclass_was_called", False): + A.__init_subclass__() diff --git a/tests/cpydiff/core_exception_construction.py b/tests/cpydiff/core_exception_construction.py new file mode 100644 index 00000000000..56a358f4c45 --- /dev/null +++ b/tests/cpydiff/core_exception_construction.py @@ -0,0 +1,28 @@ +""" +categories: Core,Exceptions +description: Throwing a derived exception class instance in its `__init__` without first calling ``super().__init__`` is a TypeError +cause: In MicroPython, an object is incompletely constructed if it does not call its superclass init function or return normally from its ``__init__``. This prevents its usage in some circumstances. +workaround: Call the superclass `__init__` method before raising the exception. +""" + + +class C(Exception): + def __init__(self): + raise self + + +class C1(Exception): + def __init__(self): + super().__init__() + raise self + + +try: + C() +except Exception as e: + print(type(e).__name__) + +try: + C1() +except Exception as e: + print(type(e).__name__) diff --git a/tests/cpydiff/core_fstring_concat.py b/tests/cpydiff/core_fstring_concat.py index 83bf18820ec..2fbe1b961a1 100644 --- a/tests/cpydiff/core_fstring_concat.py +++ b/tests/cpydiff/core_fstring_concat.py @@ -1,12 +1,14 @@ """ -categories: Core +categories: Core,f-strings description: f-strings don't support concatenation with adjacent literals if the adjacent literals contain braces cause: MicroPython is optimised for code space. workaround: Use the + operator between literal strings when they are not both f-strings """ x, y = 1, 2 +# fmt: off print("aa" f"{x}") # works print(f"{x}" "ab") # works print("a{}a" f"{x}") # fails print(f"{x}" "a{}b") # fails +# fmt: on diff --git a/tests/cpydiff/core_fstring_parser.py b/tests/cpydiff/core_fstring_parser.py index 22bbc5866ec..04b24cdfb69 100644 --- a/tests/cpydiff/core_fstring_parser.py +++ b/tests/cpydiff/core_fstring_parser.py @@ -1,9 +1,9 @@ """ -categories: Core +categories: Core,f-strings description: f-strings cannot support expressions that require parsing to resolve unbalanced nested braces and brackets cause: MicroPython is optimised for code space. workaround: Always use balanced braces and brackets in expressions inside f-strings """ -print(f'{"hello { world"}') -print(f'{"hello ] world"}') +print(f"{'hello { world'}") +print(f"{'hello ] world'}") diff --git a/tests/cpydiff/core_fstring_repr.py b/tests/cpydiff/core_fstring_repr.py index d37fb48db75..2589a34b7e3 100644 --- a/tests/cpydiff/core_fstring_repr.py +++ b/tests/cpydiff/core_fstring_repr.py @@ -1,5 +1,5 @@ """ -categories: Core +categories: Core,f-strings description: f-strings don't support !a conversions cause: MicropPython does not implement ascii() workaround: None diff --git a/tests/cpydiff/core_function_argcount.py b/tests/cpydiff/core_function_argcount.py index 5f3dca4dcdb..c257def726a 100644 --- a/tests/cpydiff/core_function_argcount.py +++ b/tests/cpydiff/core_function_argcount.py @@ -4,6 +4,7 @@ cause: MicroPython counts "self" as an argument. workaround: Interpret error messages with the information above in mind. """ + try: [].append() except Exception as e: diff --git a/tests/cpydiff/core_function_star.py b/tests/cpydiff/core_function_star.py new file mode 100644 index 00000000000..26f5effd4e3 --- /dev/null +++ b/tests/cpydiff/core_function_star.py @@ -0,0 +1,16 @@ +""" +categories: Core,Functions +description: ``*args`` cannot follow a keyword argument +cause: MicroPython is optimised for code space. For more information see `this issue `_. +workaround: Re-order the arguments +""" + + +def f(x, y): + return x + y + + +try: + print(f(y=1, *(3,))) +except Exception as e: + print(e) diff --git a/tests/cpydiff/core_import_all.py b/tests/cpydiff/core_import_all.py deleted file mode 100644 index 5adf9ae3eb1..00000000000 --- a/tests/cpydiff/core_import_all.py +++ /dev/null @@ -1,9 +0,0 @@ -""" -categories: Core,import -description: __all__ is unsupported in __init__.py in MicroPython. -cause: Not implemented. -workaround: Manually import the sub-modules directly in __init__.py using ``from . import foo, bar``. -""" -from modules3 import * - -foo.hello() diff --git a/tests/cpydiff/core_import_path.py b/tests/cpydiff/core_import_path.py index 959fd571f57..2028386be84 100644 --- a/tests/cpydiff/core_import_path.py +++ b/tests/cpydiff/core_import_path.py @@ -4,6 +4,7 @@ cause: MicroPython doesn't support namespace packages split across filesystem. Beyond that, MicroPython's import system is highly optimized for minimal memory usage. workaround: Details of import handling is inherently implementation dependent. Don't rely on such details in portable applications. """ + import modules print(modules.__path__) diff --git a/tests/cpydiff/core_import_split_ns_pkgs.py b/tests/cpydiff/core_import_split_ns_pkgs.py index 5c92b63124a..a1cfd84893d 100644 --- a/tests/cpydiff/core_import_split_ns_pkgs.py +++ b/tests/cpydiff/core_import_split_ns_pkgs.py @@ -4,6 +4,7 @@ cause: MicroPython's import system is highly optimized for simplicity, minimal memory usage, and minimal filesystem search overhead. workaround: Don't install modules belonging to the same namespace package in different directories. For MicroPython, it's recommended to have at most 3-component module search paths: for your current application, per-user (writable), system-wide (non-writable). """ + import sys sys.path.append(sys.path[1] + "/modules") diff --git a/tests/cpydiff/core_locals_eval.py b/tests/cpydiff/core_locals_eval.py index 025d2263729..3f6ad2a1dbb 100644 --- a/tests/cpydiff/core_locals_eval.py +++ b/tests/cpydiff/core_locals_eval.py @@ -4,6 +4,7 @@ cause: MicroPython doesn't maintain symbolic local environment, it is optimized to an array of slots. Thus, local variables can't be accessed by a name. Effectively, ``eval(expr)`` in MicroPython is equivalent to ``eval(expr, globals(), globals())``. workaround: Unknown """ + val = 1 diff --git a/tests/cpydiff/module_array_comparison.py b/tests/cpydiff/module_array_comparison.py index a442af3f5bc..4929b1e590d 100644 --- a/tests/cpydiff/module_array_comparison.py +++ b/tests/cpydiff/module_array_comparison.py @@ -4,6 +4,7 @@ cause: Code size workaround: Compare individual elements """ + import array array.array("b", [1, 2]) == array.array("i", [1, 2]) diff --git a/tests/cpydiff/module_array_constructor.py b/tests/cpydiff/module_array_constructor.py index 08cf2ef2d1c..a53589d22ae 100644 --- a/tests/cpydiff/module_array_constructor.py +++ b/tests/cpydiff/module_array_constructor.py @@ -4,6 +4,7 @@ cause: MicroPython implements implicit truncation in order to reduce code size and execution time workaround: If CPython compatibility is needed then mask the value explicitly """ + import array a = array.array("b", [257]) diff --git a/tests/cpydiff/modules3/__init__.py b/tests/cpydiff/modules3/__init__.py deleted file mode 100644 index 27a2bf2ad90..00000000000 --- a/tests/cpydiff/modules3/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = ["foo"] diff --git a/tests/cpydiff/modules3/foo.py b/tests/cpydiff/modules3/foo.py deleted file mode 100644 index dd9b9d4ddd4..00000000000 --- a/tests/cpydiff/modules3/foo.py +++ /dev/null @@ -1,2 +0,0 @@ -def hello(): - print("hello") diff --git a/tests/cpydiff/modules_array_containment.py b/tests/cpydiff/modules_array_containment.py index 8f25b0d4914..b29895aa758 100644 --- a/tests/cpydiff/modules_array_containment.py +++ b/tests/cpydiff/modules_array_containment.py @@ -4,6 +4,7 @@ cause: Unknown workaround: Unknown """ + import array print(1 in array.array("B", b"12")) diff --git a/tests/cpydiff/modules_array_deletion.py b/tests/cpydiff/modules_array_deletion.py index 3376527373e..2e7c31124b2 100644 --- a/tests/cpydiff/modules_array_deletion.py +++ b/tests/cpydiff/modules_array_deletion.py @@ -4,6 +4,7 @@ cause: Unknown workaround: Unknown """ + import array a = array.array("b", (1, 2, 3)) diff --git a/tests/cpydiff/modules_array_subscrstep.py b/tests/cpydiff/modules_array_subscrstep.py index 24308bd9042..65b12332f54 100644 --- a/tests/cpydiff/modules_array_subscrstep.py +++ b/tests/cpydiff/modules_array_subscrstep.py @@ -4,6 +4,7 @@ cause: Unknown workaround: Unknown """ + import array a = array.array("b", (1, 2, 3)) diff --git a/tests/cpydiff/modules_errno_enotsup.py b/tests/cpydiff/modules_errno_enotsup.py new file mode 100644 index 00000000000..80e5ad9d032 --- /dev/null +++ b/tests/cpydiff/modules_errno_enotsup.py @@ -0,0 +1,10 @@ +""" +categories: Modules,errno +description: MicroPython does not include ``ENOTSUP`` as a name for errno 95. +cause: MicroPython does not implement the ``ENOTSUP`` canonical alias for ``EOPNOTSUPP`` added in CPython 3.2. +workaround: Use ``errno.EOPNOTSUPP`` in place of ``errno.ENOTSUP``. +""" + +import errno + +print(f"{errno.errorcode[errno.EOPNOTSUPP]=!s}") diff --git a/tests/cpydiff/modules_json_nonserializable.py b/tests/cpydiff/modules_json_nonserializable.py index ffe523786f5..d83e7beca2d 100644 --- a/tests/cpydiff/modules_json_nonserializable.py +++ b/tests/cpydiff/modules_json_nonserializable.py @@ -4,12 +4,10 @@ cause: Unknown workaround: Unknown """ + import json -a = bytes(x for x in range(256)) try: - z = json.dumps(a) - x = json.loads(z) - print("Should not get here") + print(json.dumps(b"shouldn't be able to serialise bytes")) except TypeError: print("TypeError") diff --git a/tests/cpydiff/modules_os_environ.py b/tests/cpydiff/modules_os_environ.py index 491d8a31016..4edde166567 100644 --- a/tests/cpydiff/modules_os_environ.py +++ b/tests/cpydiff/modules_os_environ.py @@ -4,6 +4,7 @@ cause: Unknown workaround: Use ``getenv``, ``putenv`` and ``unsetenv`` """ + import os try: diff --git a/tests/cpydiff/modules_os_getenv.py b/tests/cpydiff/modules_os_getenv.py index d1e828438e4..d5acd414eb3 100644 --- a/tests/cpydiff/modules_os_getenv.py +++ b/tests/cpydiff/modules_os_getenv.py @@ -4,6 +4,7 @@ cause: The ``environ`` attribute is not implemented workaround: Unknown """ + import os print(os.getenv("NEW_VARIABLE")) diff --git a/tests/cpydiff/modules_struct_fewargs.py b/tests/cpydiff/modules_struct_fewargs.py index cb6b0fd874e..f6346a67938 100644 --- a/tests/cpydiff/modules_struct_fewargs.py +++ b/tests/cpydiff/modules_struct_fewargs.py @@ -1,9 +1,10 @@ """ categories: Modules,struct -description: Struct pack with too few args, not checked by uPy +description: Struct pack with too few args, not checked by MicroPython cause: Unknown workaround: Unknown """ + import struct try: diff --git a/tests/cpydiff/modules_struct_manyargs.py b/tests/cpydiff/modules_struct_manyargs.py index 03395baad3b..b2ea93b6c93 100644 --- a/tests/cpydiff/modules_struct_manyargs.py +++ b/tests/cpydiff/modules_struct_manyargs.py @@ -1,9 +1,10 @@ """ categories: Modules,struct -description: Struct pack with too many args, not checked by uPy +description: Struct pack with too many args, not checked by MicroPython cause: Unknown workaround: Unknown """ + import struct try: diff --git a/tests/cpydiff/modules_struct_whitespace_in_format.py b/tests/cpydiff/modules_struct_whitespace_in_format.py index a882b38569a..8b609425eb0 100644 --- a/tests/cpydiff/modules_struct_whitespace_in_format.py +++ b/tests/cpydiff/modules_struct_whitespace_in_format.py @@ -1,9 +1,10 @@ """ categories: Modules,struct -description: Struct pack with whitespace in format, whitespace ignored by CPython, error on uPy +description: Struct pack with whitespace in format, whitespace ignored by CPython, error on MicroPython cause: MicroPython is optimised for code size. workaround: Don't use spaces in format strings. """ + import struct try: diff --git a/tests/cpydiff/modules_sys_stdassign.py b/tests/cpydiff/modules_sys_stdassign.py index 7d086078a93..29afe1b1203 100644 --- a/tests/cpydiff/modules_sys_stdassign.py +++ b/tests/cpydiff/modules_sys_stdassign.py @@ -4,6 +4,7 @@ cause: They are stored in read-only memory. workaround: Unknown """ + import sys sys.stdin = None diff --git a/tests/cpydiff/syntax_annotation_expression.py b/tests/cpydiff/syntax_annotation_expression.py new file mode 100644 index 00000000000..91814ec9522 --- /dev/null +++ b/tests/cpydiff/syntax_annotation_expression.py @@ -0,0 +1,24 @@ +""" +categories: Syntax,Annotations +description: MicroPython accepts type annotations on expressions where CPython forbids them. +cause: To reduce code size, MicroPython does not check the form of expressions with type annotations applied. +workaround: Always check for valid Python code using a linting tool. + +The expressions themselves are not evaluated. +""" + + +def test(expr): + code = f"def f():\n {expr}: int" + print(code) + try: + exec(code) + print("OK") + except SyntaxError as e: + print("SyntaxError") + print() + + +test("print('test')") +test("[x,y]") +test("x,y") diff --git a/tests/cpydiff/syntax_arg_unpacking.py b/tests/cpydiff/syntax_arg_unpacking.py index e54832ddb91..7133a8a2827 100644 --- a/tests/cpydiff/syntax_arg_unpacking.py +++ b/tests/cpydiff/syntax_arg_unpacking.py @@ -1,5 +1,5 @@ """ -categories: Syntax +categories: Syntax,Unpacking description: Argument unpacking does not work if the argument being unpacked is the nth or greater argument where n is the number of bits in an MP_SMALL_INT. cause: The implementation uses an MP_SMALL_INT to flag args that need to be unpacked. workaround: Use fewer arguments. diff --git a/tests/cpydiff/syntax_assign_expr.py b/tests/cpydiff/syntax_assign_expr.py index d4ed063b39a..704c5c3ecab 100644 --- a/tests/cpydiff/syntax_assign_expr.py +++ b/tests/cpydiff/syntax_assign_expr.py @@ -1,7 +1,8 @@ """ categories: Syntax,Operators -description: MicroPython allows using := to assign to the variable of a comprehension, CPython raises a SyntaxError. -cause: MicroPython is optimised for code size and doesn't check this case. -workaround: Do not rely on this behaviour if writing CPython compatible code. +description: MicroPython allows := to assign to the iteration variable in nested comprehensions, CPython does not. +cause: MicroPython is optimised for code size. Although it is a syntax error to assign to the iteration variable in a standard comprehension (same as CPython), it doesn't check if an inner nested comprehension assigns to the iteration variable of the outer comprehension. +workaround: Do not use := to assign to the iteration variable of a comprehension. """ -print([i := -1 for i in range(4)]) + +print([[(j := i) for i in range(2)] for j in range(2)]) diff --git a/tests/cpydiff/syntax_literal_underscore.py b/tests/cpydiff/syntax_literal_underscore.py new file mode 100644 index 00000000000..4b1406e9f3f --- /dev/null +++ b/tests/cpydiff/syntax_literal_underscore.py @@ -0,0 +1,19 @@ +""" +categories: Syntax,Literals +description: MicroPython accepts underscores in numeric literals where CPython doesn't +cause: Different parser implementation + +MicroPython's tokenizer ignores underscores in numeric literals, while CPython +rejects multiple consecutive underscores and underscores after the last digit. + +workaround: Remove the underscores not accepted by CPython. +""" + +try: + print(eval("1__1")) +except SyntaxError: + print("Should not work") +try: + print(eval("1_")) +except SyntaxError: + print("Should not work") diff --git a/tests/cpydiff/syntax_spaces.py b/tests/cpydiff/syntax_spaces.py index c308240a78d..670cefdeac2 100644 --- a/tests/cpydiff/syntax_spaces.py +++ b/tests/cpydiff/syntax_spaces.py @@ -1,9 +1,17 @@ """ -categories: Syntax,Spaces -description: uPy requires spaces between literal numbers and keywords, CPy doesn't -cause: Unknown -workaround: Unknown +categories: Syntax,Literals +description: MicroPython requires spaces between literal numbers and keywords or ".", CPython doesn't +cause: Different parser implementation + +MicroPython's tokenizer treats a sequence like ``1and`` as a single token, while CPython treats it as two tokens. + +Since CPython 3.11, when the literal number is followed by a token, this syntax causes a ``SyntaxWarning`` for an "invalid literal". When a literal number is followed by a "." denoting attribute access, CPython does not warn. + +workaround: Add a space between the integer literal and the intended next token. + +This also fixes the ``SyntaxWarning`` in CPython. """ + try: print(eval("1and 0")) except SyntaxError: @@ -16,3 +24,7 @@ print(eval("1if 1else 0")) except SyntaxError: print("Should have worked") +try: + print(eval("0x1.to_bytes(1)")) +except SyntaxError: + print("Should have worked") diff --git a/tests/cpydiff/syntax_unicode_nameesc.py b/tests/cpydiff/syntax_unicode_nameesc.py index 21628c974d8..838394b6ebd 100644 --- a/tests/cpydiff/syntax_unicode_nameesc.py +++ b/tests/cpydiff/syntax_unicode_nameesc.py @@ -4,4 +4,5 @@ cause: Unknown workaround: Unknown """ + print("\N{LATIN SMALL LETTER A}") diff --git a/tests/cpydiff/types_bytearray_sliceassign.py b/tests/cpydiff/types_bytearray_sliceassign.py index e4068b04b98..353f61988fa 100644 --- a/tests/cpydiff/types_bytearray_sliceassign.py +++ b/tests/cpydiff/types_bytearray_sliceassign.py @@ -4,6 +4,7 @@ cause: Unknown workaround: Unknown """ + b = bytearray(4) b[0:1] = [1, 2] print(b) diff --git a/tests/cpydiff/types_bytes_format.py b/tests/cpydiff/types_bytes_format.py index ad049877116..1a15e572c8a 100644 --- a/tests/cpydiff/types_bytes_format.py +++ b/tests/cpydiff/types_bytes_format.py @@ -4,4 +4,5 @@ cause: MicroPython strives to be a more regular implementation, so if both `str` and `bytes` support ``__mod__()`` (the % operator), it makes sense to support ``format()`` for both too. Support for ``__mod__`` can also be compiled out, which leaves only ``format()`` for bytes formatting. workaround: If you are interested in CPython compatibility, don't use ``.format()`` on bytes objects. """ + print(b"{}".format(1)) diff --git a/tests/cpydiff/types_bytes_keywords.py b/tests/cpydiff/types_bytes_keywords.py index ade83d0a709..a459c94b41e 100644 --- a/tests/cpydiff/types_bytes_keywords.py +++ b/tests/cpydiff/types_bytes_keywords.py @@ -4,4 +4,5 @@ cause: Unknown workaround: Pass the encoding as a positional parameter, e.g. ``print(bytes('abc', 'utf-8'))`` """ + print(bytes("abc", encoding="utf8")) diff --git a/tests/cpydiff/types_bytes_subscrstep.py b/tests/cpydiff/types_bytes_subscrstep.py index 51b94cb710f..c566cbdb630 100644 --- a/tests/cpydiff/types_bytes_subscrstep.py +++ b/tests/cpydiff/types_bytes_subscrstep.py @@ -4,4 +4,5 @@ cause: MicroPython is highly optimized for memory usage. workaround: Use explicit loop for this very rare operation. """ + print(b"123"[0:3:2]) diff --git a/tests/cpydiff/types_complex_parser.py b/tests/cpydiff/types_complex_parser.py new file mode 100644 index 00000000000..4a012987d9e --- /dev/null +++ b/tests/cpydiff/types_complex_parser.py @@ -0,0 +1,14 @@ +""" +categories: Types,complex +description: MicroPython's complex() accepts certain incorrect values that CPython rejects +cause: MicroPython is highly optimized for memory usage. +workaround: Do not use non-standard complex literals as argument to complex() + +MicroPython's ``complex()`` function accepts literals that contain a space and +no sign between the real and imaginary parts, and interprets it as a plus. +""" + +try: + print(complex("1 1j")) +except ValueError: + print("ValueError") diff --git a/tests/cpydiff/types_dict_keys_set.py b/tests/cpydiff/types_dict_keys_set.py index 3a0849a3556..a5f127962e6 100644 --- a/tests/cpydiff/types_dict_keys_set.py +++ b/tests/cpydiff/types_dict_keys_set.py @@ -4,4 +4,5 @@ cause: Not implemented. workaround: Explicitly convert keys to a set before using set operations. """ + print({1: 2, 3: 4}.keys() & {1}) diff --git a/tests/cpydiff/types_exception_attrs.py b/tests/cpydiff/types_exception_attrs.py index ad72b62a61a..5fed45451d2 100644 --- a/tests/cpydiff/types_exception_attrs.py +++ b/tests/cpydiff/types_exception_attrs.py @@ -4,6 +4,7 @@ cause: MicroPython is optimised to reduce code size. workaround: Only use ``value`` on ``StopIteration`` exceptions, and ``errno`` on ``OSError`` exceptions. Do not use or rely on these attributes on other exceptions. """ + e = Exception(1) print(e.value) print(e.errno) diff --git a/tests/cpydiff/types_exception_chaining.py b/tests/cpydiff/types_exception_chaining.py index 836c4eb3e73..cff68d4124a 100644 --- a/tests/cpydiff/types_exception_chaining.py +++ b/tests/cpydiff/types_exception_chaining.py @@ -4,6 +4,7 @@ cause: Unknown workaround: Unknown """ + try: raise TypeError except TypeError: diff --git a/tests/cpydiff/types_exception_instancevar.py b/tests/cpydiff/types_exception_instancevar.py index adc353361f0..fb67771baf3 100644 --- a/tests/cpydiff/types_exception_instancevar.py +++ b/tests/cpydiff/types_exception_instancevar.py @@ -4,6 +4,7 @@ cause: MicroPython is highly optimized for memory usage. workaround: Use user-defined exception subclasses. """ + e = Exception() e.x = 0 print(e.x) diff --git a/tests/cpydiff/types_exception_loops.py b/tests/cpydiff/types_exception_loops.py index 8d326cbbbb0..549f1dd0a5b 100644 --- a/tests/cpydiff/types_exception_loops.py +++ b/tests/cpydiff/types_exception_loops.py @@ -4,6 +4,7 @@ cause: Condition checks are optimized to happen at the end of loop body, and that line number is reported. workaround: Unknown """ + l = ["-foo", "-bar"] i = 0 diff --git a/tests/cpydiff/types_float_implicit_conversion.py b/tests/cpydiff/types_float_implicit_conversion.py index 3726839fac6..764c9e4e6ed 100644 --- a/tests/cpydiff/types_float_implicit_conversion.py +++ b/tests/cpydiff/types_float_implicit_conversion.py @@ -1,6 +1,6 @@ """ categories: Types,float -description: uPy allows implicit conversion of objects in maths operations while CPython does not. +description: MicroPython allows implicit conversion of objects in maths operations while CPython does not. cause: Unknown workaround: Objects should be wrapped in ``float(obj)`` for compatibility with CPython. """ diff --git a/tests/cpydiff/types_float_rounding.py b/tests/cpydiff/types_float_rounding.py deleted file mode 100644 index a5b591966b0..00000000000 --- a/tests/cpydiff/types_float_rounding.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -categories: Types,float -description: uPy and CPython outputs formats may differ -cause: Unknown -workaround: Unknown -""" -print("%.1g" % -9.9) diff --git a/tests/cpydiff/types_list_delete_subscrstep.py b/tests/cpydiff/types_list_delete_subscrstep.py index 36e6f526b3d..3c801d84949 100644 --- a/tests/cpydiff/types_list_delete_subscrstep.py +++ b/tests/cpydiff/types_list_delete_subscrstep.py @@ -4,6 +4,7 @@ cause: Unknown workaround: Use explicit loop for this rare operation. """ + l = [1, 2, 3, 4] del l[0:4:2] print(l) diff --git a/tests/cpydiff/types_list_store_noniter.py b/tests/cpydiff/types_list_store_noniter.py index 1d69b4a8231..581b1369801 100644 --- a/tests/cpydiff/types_list_store_noniter.py +++ b/tests/cpydiff/types_list_store_noniter.py @@ -4,6 +4,7 @@ cause: RHS is restricted to be a tuple or list workaround: Use ``list()`` on RHS to convert the iterable to a list """ + l = [10, 20] l[0:1] = range(4) print(l) diff --git a/tests/cpydiff/types_list_store_subscrstep.py b/tests/cpydiff/types_list_store_subscrstep.py index 1460372bb17..97590267f4a 100644 --- a/tests/cpydiff/types_list_store_subscrstep.py +++ b/tests/cpydiff/types_list_store_subscrstep.py @@ -4,6 +4,7 @@ cause: Unknown workaround: Use explicit loop for this rare operation. """ + l = [1, 2, 3, 4] l[0:4:2] = [5, 6] print(l) diff --git a/tests/cpydiff/types_memoryview_invalid.py b/tests/cpydiff/types_memoryview_invalid.py index c995a2e8991..f288c50ab57 100644 --- a/tests/cpydiff/types_memoryview_invalid.py +++ b/tests/cpydiff/types_memoryview_invalid.py @@ -6,6 +6,7 @@ In the worst case scenario, resizing an object which is the target of a memoryview can cause the memoryview(s) to reference invalid freed memory (a use-after-free bug) and corrupt the MicroPython runtime. workaround: Do not change the size of any ``bytearray`` or ``io.bytesIO`` object that has a ``memoryview`` assigned to it. """ + b = bytearray(b"abcdefg") m = memoryview(b) b.extend(b"hijklmnop") diff --git a/tests/cpydiff/types_oserror_errnomap.py b/tests/cpydiff/types_oserror_errnomap.py new file mode 100644 index 00000000000..6627bd2af4a --- /dev/null +++ b/tests/cpydiff/types_oserror_errnomap.py @@ -0,0 +1,48 @@ +""" +categories: Types,OSError +description: OSError constructor returns a plain OSError for all errno values, rather than a relevant subtype. +cause: MicroPython does not include the CPython-standard OSError subclasses. +workaround: Catch OSError and use its errno attribute to discriminate the cause. +""" + +import errno + +errno_list = [ # i.e. the set implemented by micropython + errno.EPERM, + errno.ENOENT, + errno.EIO, + errno.EBADF, + errno.EAGAIN, + errno.ENOMEM, + errno.EACCES, + errno.EEXIST, + errno.ENODEV, + errno.EISDIR, + errno.EINVAL, + errno.EOPNOTSUPP, + errno.EADDRINUSE, + errno.ECONNABORTED, + errno.ECONNRESET, + errno.ENOBUFS, + errno.ENOTCONN, + errno.ETIMEDOUT, + errno.ECONNREFUSED, + errno.EHOSTUNREACH, + errno.EALREADY, + errno.EINPROGRESS, +] + + +def errno_output_type(n): + try: + raise OSError(n, "") + except OSError as e: + return f"{type(e).__name__}" + except Exception as e: + return f"non-OSError {type(e).__name__}" + else: + return "no error" + + +for n in errno_list: + print(errno.errorcode[n], "=", errno_output_type(n)) diff --git a/tests/cpydiff/types_range_limits.py b/tests/cpydiff/types_range_limits.py new file mode 100644 index 00000000000..e53d5fd4088 --- /dev/null +++ b/tests/cpydiff/types_range_limits.py @@ -0,0 +1,26 @@ +""" +categories: Types,range +description: Range objects with large start or stop arguments misbehave. +cause: Intermediate calculations overflow the C mp_int_t type +workaround: Avoid using such ranges +""" + +from sys import maxsize + +# A range including `maxsize-1` cannot be created +try: + print(range(-maxsize - 1, 0)) +except OverflowError: + print("OverflowError") + +# A range with `stop-start` exceeding sys.maxsize has incorrect len(), while CPython cannot calculate len(). +try: + print(len(range(-maxsize, maxsize))) +except OverflowError: + print("OverflowError") + +# A range with `stop-start` exceeding sys.maxsize has incorrect len() +try: + print(len(range(-maxsize, maxsize, maxsize))) +except OverflowError: + print("OverflowError") diff --git a/tests/cpydiff/types_str_endswith.py b/tests/cpydiff/types_str_endswith.py deleted file mode 100644 index f222ac1cd3a..00000000000 --- a/tests/cpydiff/types_str_endswith.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -categories: Types,str -description: Start/end indices such as str.endswith(s, start) not implemented -cause: Unknown -workaround: Unknown -""" -print("abc".endswith("c", 1)) diff --git a/tests/cpydiff/types_str_formatsep.py b/tests/cpydiff/types_str_formatsep.py new file mode 100644 index 00000000000..05d0b8d3d2c --- /dev/null +++ b/tests/cpydiff/types_str_formatsep.py @@ -0,0 +1,19 @@ +""" +categories: Types,str +description: MicroPython accepts the "," grouping option with any radix, unlike CPython +cause: To reduce code size, MicroPython does not issue an error for this combination +workaround: Do not use a format string like ``{:,b}`` if CPython compatibility is required. +""" + +try: + print("{:,b}".format(99)) +except ValueError: + print("ValueError") +try: + print("{:,x}".format(99)) +except ValueError: + print("ValueError") +try: + print("{:,o}".format(99)) +except ValueError: + print("ValueError") diff --git a/tests/cpydiff/types_str_formatsep_float.py b/tests/cpydiff/types_str_formatsep_float.py new file mode 100644 index 00000000000..b487cd3758e --- /dev/null +++ b/tests/cpydiff/types_str_formatsep_float.py @@ -0,0 +1,11 @@ +""" +categories: Types,str +description: MicroPython accepts but does not properly implement the "," or "_" grouping character for float values +cause: To reduce code size, MicroPython does not implement this combination. Grouping characters will not appear in the number's significant digits and will appear at incorrect locations in leading zeros. +workaround: Do not use a format string like ``{:,f}`` if exact CPython compatibility is required. +""" + +print("{:,f}".format(3141.159)) +print("{:_f}".format(3141.159)) +print("{:011,.2f}".format(3141.159)) +print("{:011_.2f}".format(3141.159)) diff --git a/tests/cpydiff/types_str_formatsubscr.py b/tests/cpydiff/types_str_formatsubscr.py index 1b83cfff6cd..5c59a1d200a 100644 --- a/tests/cpydiff/types_str_formatsubscr.py +++ b/tests/cpydiff/types_str_formatsubscr.py @@ -4,4 +4,5 @@ cause: Unknown workaround: Unknown """ + print("{a[0]}".format(a=[1, 2])) diff --git a/tests/cpydiff/types_str_keywords.py b/tests/cpydiff/types_str_keywords.py index 77a4eac1c1d..640dfa6c391 100644 --- a/tests/cpydiff/types_str_keywords.py +++ b/tests/cpydiff/types_str_keywords.py @@ -4,4 +4,5 @@ cause: Unknown workaround: Input the encoding format directly. eg ``print(bytes('abc', 'utf-8'))`` """ + print(str(b"abc", encoding="utf8")) diff --git a/tests/cpydiff/types_str_ljust_rjust.py b/tests/cpydiff/types_str_ljust_rjust.py index 72e5105e025..7f91c137e90 100644 --- a/tests/cpydiff/types_str_ljust_rjust.py +++ b/tests/cpydiff/types_str_ljust_rjust.py @@ -4,4 +4,5 @@ cause: MicroPython is highly optimized for memory usage. Easy workarounds available. workaround: Instead of ``s.ljust(10)`` use ``"%-10s" % s``, instead of ``s.rjust(10)`` use ``"% 10s" % s``. Alternatively, ``"{:<10}".format(s)`` or ``"{:>10}".format(s)``. """ + print("abc".ljust(10)) diff --git a/tests/cpydiff/types_str_rsplitnone.py b/tests/cpydiff/types_str_rsplitnone.py index 5d334fea2f8..ae524620a08 100644 --- a/tests/cpydiff/types_str_rsplitnone.py +++ b/tests/cpydiff/types_str_rsplitnone.py @@ -4,4 +4,5 @@ cause: Unknown workaround: Unknown """ + print("a a a".rsplit(None, 1)) diff --git a/tests/cpydiff/types_str_subscrstep.py b/tests/cpydiff/types_str_subscrstep.py index 2d3245e5582..cd57f18ccb3 100644 --- a/tests/cpydiff/types_str_subscrstep.py +++ b/tests/cpydiff/types_str_subscrstep.py @@ -4,4 +4,5 @@ cause: Unknown workaround: Unknown """ + print("abcdefghi"[0:9:2]) diff --git a/tests/cpydiff/types_tuple_subscrstep.py b/tests/cpydiff/types_tuple_subscrstep.py index f90f3c5bf4d..60ba31af4cf 100644 --- a/tests/cpydiff/types_tuple_subscrstep.py +++ b/tests/cpydiff/types_tuple_subscrstep.py @@ -4,4 +4,5 @@ cause: Unknown workaround: Unknown """ + print((1, 2, 3, 4)[0:4:2]) diff --git a/tests/extmod/asyncio_basic.py.exp b/tests/extmod/asyncio_basic.py.exp deleted file mode 100644 index 478e22abc8f..00000000000 --- a/tests/extmod/asyncio_basic.py.exp +++ /dev/null @@ -1,6 +0,0 @@ -start -after sleep -short -long -negative -took 200 400 0 diff --git a/tests/extmod/asyncio_event_queue.py b/tests/extmod/asyncio_event_queue.py new file mode 100644 index 00000000000..e0125b1aefe --- /dev/null +++ b/tests/extmod/asyncio_event_queue.py @@ -0,0 +1,64 @@ +# Ensure that an asyncio task can wait on an Event when the +# _task_queue is empty +# https://github.com/micropython/micropython/issues/16569 + +try: + import asyncio +except ImportError: + print("SKIP") + raise SystemExit + +# This test requires checking that the asyncio scheduler +# remains active "indefinitely" when the task queue is empty. +# +# To check this, we need another independent scheduler that +# can wait for a certain amount of time. So we have to +# create one using micropython.schedule() and time.ticks_ms() +# +# Technically, this code breaks the rules, as it is clearly +# documented that Event.set() should _NOT_ be called from a +# schedule (soft IRQ) because in some cases, a race condition +# can occur, resulting in a crash. However: +# - since the risk of a race condition in that specific +# case has been analysed and excluded +# - given that there is no other simple alternative to +# write this test case, +# an exception to the rule was deemed acceptable. See +# https://github.com/micropython/micropython/pull/16772 + +import micropython, time + +try: + micropython.schedule +except AttributeError: + print("SKIP") + raise SystemExit + + +evt = asyncio.Event() + + +def schedule_watchdog(end_ticks): + if time.ticks_diff(end_ticks, time.ticks_ms()) <= 0: + print("asyncio still pending, unlocking event") + # Caution: about to call Event.set() from a schedule + # (see the note in the comment above) + evt.set() + return + micropython.schedule(schedule_watchdog, end_ticks) + + +async def foo(): + print("foo waiting") + schedule_watchdog(time.ticks_add(time.ticks_ms(), 100)) + await evt.wait() + print("foo done") + + +async def main(): + print("main started") + await foo() + print("main done") + + +asyncio.run(main()) diff --git a/tests/extmod/asyncio_event_queue.py.exp b/tests/extmod/asyncio_event_queue.py.exp new file mode 100644 index 00000000000..ee42c96d83e --- /dev/null +++ b/tests/extmod/asyncio_event_queue.py.exp @@ -0,0 +1,5 @@ +main started +foo waiting +asyncio still pending, unlocking event +foo done +main done diff --git a/tests/extmod/asyncio_heaplock.py b/tests/extmod/asyncio_heaplock.py index 8326443f0e6..9e9908de1cb 100644 --- a/tests/extmod/asyncio_heaplock.py +++ b/tests/extmod/asyncio_heaplock.py @@ -4,7 +4,11 @@ # - StreamWriter.write, stream is blocked and data to write is a bytes object # - StreamWriter.write, when stream is not blocked -import micropython +try: + import asyncio, micropython +except ImportError: + print("SKIP") + raise SystemExit # strict stackless builds can't call functions without allocating a frame on the heap try: @@ -24,12 +28,6 @@ def f(x): print("SKIP") raise SystemExit -try: - import asyncio -except ImportError: - print("SKIP") - raise SystemExit - class TestStream: def __init__(self, blocked): diff --git a/tests/extmod/asyncio_iterator_event.py b/tests/extmod/asyncio_iterator_event.py new file mode 100644 index 00000000000..f61fefcf051 --- /dev/null +++ b/tests/extmod/asyncio_iterator_event.py @@ -0,0 +1,86 @@ +# Ensure that an asyncio task can wait on an Event when the +# _task_queue is empty, in the context of an async iterator +# https://github.com/micropython/micropython/issues/16318 + +try: + import asyncio +except ImportError: + print("SKIP") + raise SystemExit + +# This test requires checking that the asyncio scheduler +# remains active "indefinitely" when the task queue is empty. +# +# To check this, we need another independent scheduler that +# can wait for a certain amount of time. So we have to +# create one using micropython.schedule() and time.ticks_ms() +# +# Technically, this code breaks the rules, as it is clearly +# documented that Event.set() should _NOT_ be called from a +# schedule (soft IRQ) because in some cases, a race condition +# can occur, resulting in a crash. However: +# - since the risk of a race condition in that specific +# case has been analysed and excluded +# - given that there is no other simple alternative to +# write this test case, +# an exception to the rule was deemed acceptable. See +# https://github.com/micropython/micropython/pull/16772 + +import micropython, time + +try: + micropython.schedule +except AttributeError: + print("SKIP") + raise SystemExit + +ai = None + + +def schedule_watchdog(end_ticks): + if time.ticks_diff(end_ticks, time.ticks_ms()) <= 0: + print("good: asyncio iterator is still pending, exiting") + # Caution: ai.fetch_data() will invoke Event.set() + # (see the note in the comment above) + ai.fetch_data(None) + return + micropython.schedule(schedule_watchdog, end_ticks) + + +async def test(ai): + for x in range(3): + await asyncio.sleep(0.1) + ai.fetch_data("bar {}".format(x)) + + +class AsyncIterable: + def __init__(self): + self.message = None + self.evt = asyncio.Event() + + def __aiter__(self): + return self + + async def __anext__(self): + await self.evt.wait() + self.evt.clear() + if self.message is None: + raise StopAsyncIteration + return self.message + + def fetch_data(self, message): + self.message = message + self.evt.set() + + +async def main(): + global ai + ai = AsyncIterable() + asyncio.create_task(test(ai)) + schedule_watchdog(time.ticks_add(time.ticks_ms(), 500)) + async for message in ai: + print(message) + print("end main") + + +asyncio.run(main()) diff --git a/tests/extmod/asyncio_iterator_event.py.exp b/tests/extmod/asyncio_iterator_event.py.exp new file mode 100644 index 00000000000..a1893197d02 --- /dev/null +++ b/tests/extmod/asyncio_iterator_event.py.exp @@ -0,0 +1,5 @@ +bar 0 +bar 1 +bar 2 +good: asyncio iterator is still pending, exiting +end main diff --git a/tests/extmod/asyncio_lock.py.exp b/tests/extmod/asyncio_lock.py.exp deleted file mode 100644 index a37dfcbd2e5..00000000000 --- a/tests/extmod/asyncio_lock.py.exp +++ /dev/null @@ -1,41 +0,0 @@ -False -True -False -have lock ----- -task start 1 -task start 2 -task start 3 -task have 1 0 -task have 2 0 -task have 3 0 -task have 1 1 -task have 2 1 -task have 3 1 -task have 1 2 -task end 1 -task have 2 2 -task end 2 -task have 3 2 -task end 3 ----- -task have True -task release False -task have True -task release False -task have again -task have again ----- -task got 0 -task release 0 -task cancel 1 -task got 2 -task release 2 -False ----- -task got 0 -task cancel 1 -task release 0 -task got 2 -task cancel 2 -False diff --git a/tests/extmod/asyncio_new_event_loop.py b/tests/extmod/asyncio_new_event_loop.py index bebc3bf70cc..3f05ffdd551 100644 --- a/tests/extmod/asyncio_new_event_loop.py +++ b/tests/extmod/asyncio_new_event_loop.py @@ -12,6 +12,16 @@ asyncio.set_event_loop(asyncio.new_event_loop()) +def exception_handler(loop, context): + # This is a workaround for a difference between CPython and MicroPython: if + # a CPython event loop is closed while there are tasks pending (i.e. not finished) + # on it, then the task will log an error. MicroPython does not log this error. + if context.get("message", "") == "Task was destroyed but it is pending!": + pass + else: + loop.default_exception_handler(context) + + async def task(): for i in range(4): print("task", i) @@ -22,17 +32,21 @@ async def task(): async def main(): print("start") loop.create_task(task()) - await asyncio.sleep(0) + await asyncio.sleep(0) # yields, meaning new task will run once print("stop") loop.stop() # Use default event loop to run some tasks loop = asyncio.get_event_loop() +loop.set_exception_handler(exception_handler) loop.create_task(main()) loop.run_forever() +loop.close() # Create new event loop, old one should not keep running loop = asyncio.new_event_loop() +loop.set_exception_handler(exception_handler) loop.create_task(main()) loop.run_forever() +loop.close() diff --git a/tests/extmod/asyncio_set_exception_handler.py b/tests/extmod/asyncio_set_exception_handler.py index 5935f0f4ebe..0ac4a624224 100644 --- a/tests/extmod/asyncio_set_exception_handler.py +++ b/tests/extmod/asyncio_set_exception_handler.py @@ -12,7 +12,7 @@ def custom_handler(loop, context): async def task(i): - # Raise with 2 args so exception prints the same in uPy and CPython + # Raise with 2 args so exception prints the same in MicroPython and CPython raise ValueError(i, i + 1) diff --git a/tests/extmod/asyncio_wait_for_linked_task.py b/tests/extmod/asyncio_wait_for_linked_task.py new file mode 100644 index 00000000000..4dda62d5476 --- /dev/null +++ b/tests/extmod/asyncio_wait_for_linked_task.py @@ -0,0 +1,66 @@ +# Test asyncio.wait_for, with dependent tasks +# https://github.com/micropython/micropython/issues/16759 + +try: + import asyncio +except ImportError: + print("SKIP") + raise SystemExit + + +# CPython 3.12 deprecated calling get_event_loop() when there is no current event +# loop, so to make this test run on CPython requires setting the event loop. +if hasattr(asyncio, "set_event_loop"): + asyncio.set_event_loop(asyncio.new_event_loop()) + + +class Worker: + def __init__(self): + self._eventLoop = None + self._tasks = [] + + def launchTask(self, asyncJob): + if self._eventLoop is None: + self._eventLoop = asyncio.get_event_loop() + return self._eventLoop.create_task(asyncJob) + + async def job(self, prerequisite, taskName): + if prerequisite: + await prerequisite + await asyncio.sleep(0.1) + print(taskName, "work completed") + + def planTasks(self): + self._tasks.append(self.launchTask(self.job(None, "task0"))) + self._tasks.append(self.launchTask(self.job(self._tasks[0], "task1"))) + self._tasks.append(self.launchTask(self.job(self._tasks[1], "task2"))) + + async def waitForTask(self, taskIdx): + return await self._tasks[taskIdx] + + def syncWaitForTask(self, taskIdx): + return self._eventLoop.run_until_complete(self._tasks[taskIdx]) + + +async def async_test(): + print("--- async test") + worker = Worker() + worker.planTasks() + await worker.waitForTask(0) + print("-> task0 done") + await worker.waitForTask(2) + print("-> task2 done") + + +def sync_test(): + print("--- sync test") + worker = Worker() + worker.planTasks() + worker.syncWaitForTask(0) + print("-> task0 done") + worker.syncWaitForTask(2) + print("-> task2 done") + + +asyncio.get_event_loop().run_until_complete(async_test()) +sync_test() diff --git a/tests/extmod/asyncio_wait_task.py.exp b/tests/extmod/asyncio_wait_task.py.exp deleted file mode 100644 index 514a4342233..00000000000 --- a/tests/extmod/asyncio_wait_task.py.exp +++ /dev/null @@ -1,12 +0,0 @@ -start -task 1 -task 2 ----- -start -hello -world -took 200 200 -task_raise -ValueError -task_raise -ValueError diff --git a/tests/extmod/binascii_hexlify.py b/tests/extmod/binascii_hexlify.py index d06029aabaf..ae90b673365 100644 --- a/tests/extmod/binascii_hexlify.py +++ b/tests/extmod/binascii_hexlify.py @@ -1,5 +1,5 @@ try: - import binascii + from binascii import hexlify except ImportError: print("SKIP") raise SystemExit @@ -10,10 +10,10 @@ b"\x7f\x80\xff", b"1234ABCDabcd", ): - print(binascii.hexlify(x)) + print(hexlify(x)) # Two-argument version (now supported in CPython) -print(binascii.hexlify(b"123", ":")) +print(hexlify(b"123", ":")) # zero length buffer -print(binascii.hexlify(b"", b":")) +print(hexlify(b"", b":")) diff --git a/tests/extmod/binascii_unhexlify.py b/tests/extmod/binascii_unhexlify.py index bb663bc5b0c..ec31fb2325a 100644 --- a/tests/extmod/binascii_unhexlify.py +++ b/tests/extmod/binascii_unhexlify.py @@ -1,5 +1,5 @@ try: - import binascii + from binascii import unhexlify except ImportError: print("SKIP") raise SystemExit @@ -10,14 +10,14 @@ b"7f80ff", b"313233344142434461626364", ): - print(binascii.unhexlify(x)) + print(unhexlify(x)) try: - a = binascii.unhexlify(b"0") # odd buffer length + a = unhexlify(b"0") # odd buffer length except ValueError: print("ValueError") try: - a = binascii.unhexlify(b"gg") # digit not hex + a = unhexlify(b"gg") # digit not hex except ValueError: print("ValueError") diff --git a/tests/extmod/deflate_compress_memory_error.py b/tests/extmod/deflate_compress_memory_error.py new file mode 100644 index 00000000000..19bef87bff3 --- /dev/null +++ b/tests/extmod/deflate_compress_memory_error.py @@ -0,0 +1,39 @@ +# Test deflate.DeflateIO compression, with out-of-memory errors. + +try: + # Check if deflate is available. + import deflate + import io +except ImportError: + print("SKIP") + raise SystemExit + +# Check if compression is enabled. +if not hasattr(deflate.DeflateIO, "write"): + print("SKIP") + raise SystemExit + +# Create a compressor object. +b = io.BytesIO() +g = deflate.DeflateIO(b, deflate.RAW, 15) + +# Then, use up most of the heap. +l = [] +while True: + try: + l.append(bytearray(1000)) + except: + break +l.pop() + +# Try to compress. This will try to allocate a large window and fail. +try: + g.write("test") +except MemoryError: + print("MemoryError") + +# Should still be able to close the stream. +g.close() + +# The underlying output stream should be unchanged. +print(b.getvalue()) diff --git a/tests/extmod/deflate_compress_memory_error.py.exp b/tests/extmod/deflate_compress_memory_error.py.exp new file mode 100644 index 00000000000..606315c1460 --- /dev/null +++ b/tests/extmod/deflate_compress_memory_error.py.exp @@ -0,0 +1,2 @@ +MemoryError +b'' diff --git a/tests/extmod/framebuf_blit.py b/tests/extmod/framebuf_blit.py new file mode 100644 index 00000000000..b1d98b330a8 --- /dev/null +++ b/tests/extmod/framebuf_blit.py @@ -0,0 +1,68 @@ +# Test FrameBuffer.blit method. + +try: + import framebuf +except ImportError: + print("SKIP") + raise SystemExit + + +def printbuf(): + print("--8<--") + for y in range(h): + for x in range(w): + print("%02x" % buf[(x + y * w)], end="") + print() + print("-->8--") + + +w = 5 +h = 4 +buf = bytearray(w * h) +fbuf = framebuf.FrameBuffer(buf, w, h, framebuf.GS8) + +fbuf2 = framebuf.FrameBuffer(bytearray(4), 2, 2, framebuf.GS8) +fbuf2.fill(0xFF) + +# Blit another FrameBuffer, at various locations. +for x, y in ((-1, -1), (0, 0), (1, 1), (4, 3)): + fbuf.fill(0) + fbuf.blit(fbuf2, x, y) + printbuf() + +# Blit a bytes object. +fbuf.fill(0) +image = (b"\x10\x11\x12\x13", 2, 2, framebuf.GS8) +fbuf.blit(image, 1, 1) +printbuf() + +# Blit a bytes object that has a stride. +fbuf.fill(0) +image = (b"\x20\x21\xff\x22\x23\xff", 2, 2, framebuf.GS8, 3) +fbuf.blit(image, 1, 1) +printbuf() + +# Blit a bytes object with a bytes palette. +fbuf.fill(0) +image = (b"\x00\x01\x01\x00", 2, 2, framebuf.GS8) +palette = (b"\xa1\xa2", 2, 1, framebuf.GS8) +fbuf.blit(image, 1, 1, -1, palette) +printbuf() + +# Not enough elements in the tuple. +try: + fbuf.blit((0, 0, 0), 0, 0) +except ValueError: + print("ValueError") + +# Too many elements in the tuple. +try: + fbuf.blit((0, 0, 0, 0, 0, 0), 0, 0) +except ValueError: + print("ValueError") + +# Bytes too small. +try: + fbuf.blit((b"", 1, 1, framebuf.GS8), 0, 0) +except ValueError: + print("ValueError") diff --git a/tests/extmod/framebuf_blit.py.exp b/tests/extmod/framebuf_blit.py.exp new file mode 100644 index 00000000000..e340f1990c7 --- /dev/null +++ b/tests/extmod/framebuf_blit.py.exp @@ -0,0 +1,45 @@ +--8<-- +ff00000000 +0000000000 +0000000000 +0000000000 +-->8-- +--8<-- +ffff000000 +ffff000000 +0000000000 +0000000000 +-->8-- +--8<-- +0000000000 +00ffff0000 +00ffff0000 +0000000000 +-->8-- +--8<-- +0000000000 +0000000000 +0000000000 +00000000ff +-->8-- +--8<-- +0000000000 +0010110000 +0012130000 +0000000000 +-->8-- +--8<-- +0000000000 +0020210000 +0022230000 +0000000000 +-->8-- +--8<-- +0000000000 +00a1a20000 +00a2a10000 +0000000000 +-->8-- +ValueError +ValueError +ValueError diff --git a/tests/extmod/framebuf_polygon.py b/tests/extmod/framebuf_polygon.py index da05be2c4db..91a64d74c30 100644 --- a/tests/extmod/framebuf_polygon.py +++ b/tests/extmod/framebuf_polygon.py @@ -11,6 +11,12 @@ print("SKIP") raise SystemExit +try: + buf = bytearray(70 * 70) +except MemoryError: + print("SKIP") + raise SystemExit + def print_buffer(buffer, width, height): for row in range(height): @@ -20,8 +26,6 @@ def print_buffer(buffer, width, height): print() -buf = bytearray(70 * 70) - w = 30 h = 25 fbuf = framebuf.FrameBuffer(buf, w, h, framebuf.GS8) diff --git a/tests/extmod/framebuf_scroll.py b/tests/extmod/framebuf_scroll.py index db9b6ea1e96..d7c07b1c657 100644 --- a/tests/extmod/framebuf_scroll.py +++ b/tests/extmod/framebuf_scroll.py @@ -42,4 +42,9 @@ def prepare_buffer(): fbuf.scroll(15, 7) fbuf.scroll(10, -1) fbuf.scroll(1, -10) +try: + fbuf.scroll(1000000000000, -1) +except OverflowError: + # When mp_int_t is 32 bits, this throws OverflowError. + pass printbuf() diff --git a/tests/extmod/hashlib_md5.py b/tests/extmod/hashlib_md5.py index 5f925fc97d0..ebbe9155e04 100644 --- a/tests/extmod/hashlib_md5.py +++ b/tests/extmod/hashlib_md5.py @@ -1,8 +1,7 @@ try: import hashlib except ImportError: - # This is neither uPy, nor cPy, so must be uPy with - # hashlib module disabled. + # MicroPython with hashlib module disabled. print("SKIP") raise SystemExit diff --git a/tests/extmod/hashlib_sha1.py b/tests/extmod/hashlib_sha1.py index af23033a591..46ffb73fcbe 100644 --- a/tests/extmod/hashlib_sha1.py +++ b/tests/extmod/hashlib_sha1.py @@ -1,8 +1,7 @@ try: import hashlib except ImportError: - # This is neither uPy, nor cPy, so must be uPy with - # hashlib module disabled. + # MicroPython with hashlib module disabled. print("SKIP") raise SystemExit diff --git a/tests/extmod/hashlib_sha256.py b/tests/extmod/hashlib_sha256.py index f0d07e1482c..ffa0922a1cf 100644 --- a/tests/extmod/hashlib_sha256.py +++ b/tests/extmod/hashlib_sha256.py @@ -1,8 +1,7 @@ try: import hashlib except ImportError: - # This is neither uPy, nor cPy, so must be uPy with - # hashlib module disabled. + # MicroPython with hashlib module disabled. print("SKIP") raise SystemExit diff --git a/tests/extmod/json_dump.py b/tests/extmod/json_dump.py index 897d33cc812..0beb4f5f856 100644 --- a/tests/extmod/json_dump.py +++ b/tests/extmod/json_dump.py @@ -16,11 +16,11 @@ # dump to a small-int not allowed try: json.dump(123, 1) -except (AttributeError, OSError): # CPython and uPy have different errors +except (AttributeError, OSError): # CPython and MicroPython have different errors print("Exception") # dump to an object not allowed try: json.dump(123, {}) -except (AttributeError, OSError): # CPython and uPy have different errors +except (AttributeError, OSError): # CPython and MicroPython have different errors print("Exception") diff --git a/tests/extmod/json_dump_iobase.py b/tests/extmod/json_dump_iobase.py index 94d317b8796..81105e36dcc 100644 --- a/tests/extmod/json_dump_iobase.py +++ b/tests/extmod/json_dump_iobase.py @@ -18,7 +18,7 @@ def __init__(self): def write(self, buf): if type(buf) == bytearray: - # uPy passes a bytearray, CPython passes a str + # MicroPython passes a bytearray, CPython passes a str buf = str(buf, "ascii") self.buf += buf return len(buf) diff --git a/tests/extmod/json_dump_separators.py b/tests/extmod/json_dump_separators.py index 4f8e56dceb5..ce39294820f 100644 --- a/tests/extmod/json_dump_separators.py +++ b/tests/extmod/json_dump_separators.py @@ -25,20 +25,20 @@ # dump to a small-int not allowed try: json.dump(123, 1, separators=sep) - except (AttributeError, OSError): # CPython and uPy have different errors + except (AttributeError, OSError): # CPython and MicroPython have different errors print("Exception") # dump to an object not allowed try: json.dump(123, {}, separators=sep) - except (AttributeError, OSError): # CPython and uPy have different errors + except (AttributeError, OSError): # CPython and MicroPython have different errors print("Exception") try: s = StringIO() json.dump(False, s, separators={"a": 1}) -except (TypeError, ValueError): # CPython and uPy have different errors +except (TypeError, ValueError): # CPython and MicroPython have different errors print("Exception") # invalid separator types diff --git a/tests/extmod/json_dumps_extra.py b/tests/extmod/json_dumps_extra.py index 9074416a99d..9faf8eefabb 100644 --- a/tests/extmod/json_dumps_extra.py +++ b/tests/extmod/json_dumps_extra.py @@ -1,4 +1,4 @@ -# test uPy json behaviour that's not valid in CPy +# test MicroPython json behaviour that's not valid in CPython try: import json diff --git a/tests/extmod/json_dumps_separators.py b/tests/extmod/json_dumps_separators.py index a3a9ec308f0..0a95f489a08 100644 --- a/tests/extmod/json_dumps_separators.py +++ b/tests/extmod/json_dumps_separators.py @@ -39,7 +39,7 @@ try: json.dumps(False, separators={"a": 1}) -except (TypeError, ValueError): # CPython and uPy have different errors +except (TypeError, ValueError): # CPython and MicroPython have different errors print("Exception") # invalid separator types diff --git a/tests/extmod/json_loads.py b/tests/extmod/json_loads.py index f9073c121e2..092402d715d 100644 --- a/tests/extmod/json_loads.py +++ b/tests/extmod/json_loads.py @@ -71,3 +71,27 @@ def my_print(o): my_print(json.loads("[null] a")) except ValueError: print("ValueError") + +# incomplete object declaration +try: + my_print(json.loads('{"a":0,')) +except ValueError: + print("ValueError") + +# incomplete nested array declaration +try: + my_print(json.loads('{"a":0, [')) +except ValueError: + print("ValueError") + +# incomplete array declaration +try: + my_print(json.loads("[0,")) +except ValueError: + print("ValueError") + +# incomplete nested object declaration +try: + my_print(json.loads('[0, {"a":0, ')) +except ValueError: + print("ValueError") diff --git a/tests/extmod/json_loads_bytes.py.exp b/tests/extmod/json_loads_bytes.py.exp deleted file mode 100644 index c2735a99052..00000000000 --- a/tests/extmod/json_loads_bytes.py.exp +++ /dev/null @@ -1,2 +0,0 @@ -[1, 2] -[None] diff --git a/tests/extmod/json_loads_int_64.py b/tests/extmod/json_loads_int_64.py new file mode 100644 index 00000000000..f6236f1904a --- /dev/null +++ b/tests/extmod/json_loads_int_64.py @@ -0,0 +1,16 @@ +# Parse 64-bit integers from JSON payloads. +# +# This also exercises parsing integers from strings +# where the value may not be null terminated (last line) +try: + import json +except ImportError: + print("SKIP") + raise SystemExit + + +print(json.loads("9111222333444555666")) +print(json.loads("-9111222333444555666")) +print(json.loads("9111222333444555666")) +print(json.loads("-9111222333444555666")) +print(json.loads('["9111222333444555666777",9111222333444555666]')) diff --git a/tests/extmod/machine1.py b/tests/extmod/machine1.py index 90e6d17174f..2f1c0681f64 100644 --- a/tests/extmod/machine1.py +++ b/tests/extmod/machine1.py @@ -8,39 +8,36 @@ print("SKIP") raise SystemExit -print(machine.mem8) +import unittest -try: - machine.mem16[1] -except ValueError: - print("ValueError") -try: - machine.mem16[1] = 1 -except ValueError: - print("ValueError") +class Test(unittest.TestCase): + def test_mem8_print(self): + self.assertEqual(repr(machine.mem8), "<8-bit memory>") -try: - del machine.mem8[0] -except TypeError: - print("TypeError") + def test_alignment(self): + with self.assertRaises(ValueError): + machine.mem16[1] -try: - machine.mem8[0:1] -except TypeError: - print("TypeError") + with self.assertRaises(ValueError): + machine.mem16[1] = 1 -try: - machine.mem8[0:1] = 10 -except TypeError: - print("TypeError") + def test_operations(self): + with self.assertRaises(TypeError): + del machine.mem8[0] -try: - machine.mem8["hello"] -except TypeError: - print("TypeError") + with self.assertRaises(TypeError): + machine.mem8[0:1] -try: - machine.mem8["hello"] = 10 -except TypeError: - print("TypeError") + with self.assertRaises(TypeError): + machine.mem8[0:1] = 10 + + with self.assertRaises(TypeError): + machine.mem8["hello"] + + with self.assertRaises(TypeError): + machine.mem8["hello"] = 10 + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/extmod/machine_hard_timer.py b/tests/extmod/machine_hard_timer.py new file mode 100644 index 00000000000..8fe42ea8508 --- /dev/null +++ b/tests/extmod/machine_hard_timer.py @@ -0,0 +1,45 @@ +import sys + +try: + from machine import Timer + from time import sleep_ms +except: + print("SKIP") + raise SystemExit + +if sys.platform == "esp8266": + timer = Timer(0) +else: + # Hardware timers are not implemented. + print("SKIP") + raise SystemExit + +# Test both hard and soft IRQ handlers and both one-shot and periodic +# timers. We adjust period in tests/extmod/machine_soft_timer.py, so try +# adjusting freq here instead. The heap should be locked in hard callbacks +# and unlocked in soft callbacks. + + +def callback(t): + print("callback", mode[1], kind[1], freq, end=" ") + try: + allocate = bytearray(1) + print("unlocked") + except MemoryError: + print("locked") + + +modes = [(Timer.ONE_SHOT, "one-shot"), (Timer.PERIODIC, "periodic")] +kinds = [(False, "soft"), (True, "hard")] + +for mode in modes: + for kind in kinds: + for freq in 50, 25: + timer.init( + mode=mode[0], + freq=freq, + hard=kind[0], + callback=callback, + ) + sleep_ms(90) + timer.deinit() diff --git a/tests/extmod/machine_hard_timer.py.exp b/tests/extmod/machine_hard_timer.py.exp new file mode 100644 index 00000000000..26cdc644fdd --- /dev/null +++ b/tests/extmod/machine_hard_timer.py.exp @@ -0,0 +1,16 @@ +callback one-shot soft 50 unlocked +callback one-shot soft 25 unlocked +callback one-shot hard 50 locked +callback one-shot hard 25 locked +callback periodic soft 50 unlocked +callback periodic soft 50 unlocked +callback periodic soft 50 unlocked +callback periodic soft 50 unlocked +callback periodic soft 25 unlocked +callback periodic soft 25 unlocked +callback periodic hard 50 locked +callback periodic hard 50 locked +callback periodic hard 50 locked +callback periodic hard 50 locked +callback periodic hard 25 locked +callback periodic hard 25 locked diff --git a/tests/extmod/machine_i2s_rate.py b/tests/extmod/machine_i2s_rate.py index c8fa11514c9..f6de3d8f13c 100644 --- a/tests/extmod/machine_i2s_rate.py +++ b/tests/extmod/machine_i2s_rate.py @@ -26,7 +26,11 @@ (2, Pin("D4"), Pin("D3"), Pin("D2"), None), ) elif "esp32" in sys.platform: - i2s_instances = ((0, Pin(18), Pin(19), Pin(21), Pin(14)),) + try: + i2s_instances = ((0, Pin(18), Pin(19), Pin(21), Pin(14)),) + except ValueError: + # fallback to lower pin number for ESP32-C3 + i2s_instances = ((0, Pin(6), Pin(7), Pin(10), Pin(11)),) # Allow for small additional RTOS overhead MAX_DELTA_MS = 8 diff --git a/tests/extmod/machine_soft_timer.py b/tests/extmod/machine_soft_timer.py index 1e9c66aa225..4c0611caedf 100644 --- a/tests/extmod/machine_soft_timer.py +++ b/tests/extmod/machine_soft_timer.py @@ -1,20 +1,18 @@ # test "soft" machine.Timer (no hardware ID) import sys - -if sys.platform in ("esp32", "esp8266"): - print("SKIP") # TODO: Implement soft timers for esp32/esp8266 ports - raise SystemExit - - try: - import time, machine as machine + import time, machine machine.Timer except: print("SKIP") raise SystemExit +if sys.platform in ("esp32", "esp8266", "nrf"): + print("SKIP") # TODO: Implement soft timers for esp32/esp8266/nrf ports + raise SystemExit + # create and deinit t = machine.Timer(freq=1) t.deinit() diff --git a/tests/extmod/machine_spi_rate.py b/tests/extmod/machine_spi_rate.py index 7022955a3da..27174bf95e7 100644 --- a/tests/extmod/machine_spi_rate.py +++ b/tests/extmod/machine_spi_rate.py @@ -1,37 +1,30 @@ # Test machine.SPI data transfer rates -import sys - try: - import time from machine import Pin, SPI except ImportError: print("SKIP") raise SystemExit +try: + wr_short = b"abcdefghijklmnop" * 10 + rd_short = bytearray(len(wr_short)) + wr_long = wr_short * 20 + rd_long = bytearray(len(wr_long)) +except MemoryError: + print("SKIP") + raise SystemExit + +import time, sys +from target_wiring import spi_standalone_args_list + MAX_DELTA_MS = 8 -# Configure pins based on the port/board details. -# Values are tuples of (spi_id, sck, mosi, miso) -if "pyboard" in sys.platform: - spi_instances = ( - (1, None, None, None), # "explicit choice of sck/mosi/miso is not implemented" - (2, None, None, None), - ) -elif "rp2" in sys.platform: - spi_instances = ((0, Pin(18), Pin(19), Pin(16)),) -elif "esp32" in sys.platform: - impl = str(sys.implementation) - if "ESP32C3" in impl or "ESP32C6" in impl: - spi_instances = ((1, Pin(4), Pin(5), Pin(6)),) - else: - spi_instances = ((1, Pin(18), Pin(19), Pin(21)), (2, Pin(18), Pin(19), Pin(21))) +# Tune test parameters based on the target. +if "alif" in sys.platform: + MAX_DELTA_MS = 20 elif "esp8266" in sys.platform: MAX_DELTA_MS = 50 # port requires much looser timing requirements - spi_instances = ((1, None, None, None),) # explicit pin choice not allowed -else: - print("Please add support for this test on this platform.") - raise SystemExit def get_real_baudrate(spi): @@ -51,7 +44,7 @@ def get_real_baudrate(spi): def test_instances(): print_results = True - for spi_id, sck, mosi, miso in spi_instances: + for spi_args in spi_standalone_args_list: for baudrate in ( 100_000, 250_000, @@ -61,55 +54,19 @@ def test_instances(): 4_000_000, 8_000_000, ): - test_spi( - spi_id, - sck, - mosi, - miso, - baudrate, - 0, - 0, - print_results, - ) + test_spi(spi_args, baudrate, 0, 0, print_results) for baudrate in (100_000, 4_000_000): # Test the other polarity and phase settings for polarity, phase in ((0, 1), (1, 0), (1, 1)): - test_spi( - spi_id, - sck, - mosi, - miso, - baudrate, - polarity, - phase, - print_results, - ) + test_spi(spi_args, baudrate, polarity, phase, print_results) # Ensure the same test output regardless of how many SPI instances are tested print_results = False -wr_short = b"abcdefghijklmnop" * 10 -rd_short = bytearray(len(wr_short)) - -wr_long = wr_short * 20 -rd_long = bytearray(len(wr_long)) - - -def test_spi(spi_id, sck, mosi, miso, baudrate, polarity, phase, print_results): - if sck: - s = SPI( - spi_id, - sck=sck, - mosi=mosi, - miso=miso, - baudrate=baudrate, - polarity=polarity, - phase=phase, - ) - else: - s = SPI(spi_id, baudrate=baudrate, polarity=polarity, phase=phase) +def test_spi(spi_args, baudrate, polarity, phase, print_results): + s = SPI(*spi_args, baudrate=baudrate, polarity=polarity, phase=phase) # to keep the test runtime down, use shorter buffer for lower baud rate wr_buf = wr_long if baudrate > 500_000 else wr_short @@ -141,7 +98,7 @@ def test_readinto(): if print_results or not t_ok: print( - None if print_results else spi_id, + None if print_results else spi_args, baudrate, polarity, phase, diff --git a/tests/extmod/machine_timer.py b/tests/extmod/machine_timer.py new file mode 100644 index 00000000000..ef97ea4e949 --- /dev/null +++ b/tests/extmod/machine_timer.py @@ -0,0 +1,48 @@ +import sys + +try: + from machine import Timer + from time import sleep_ms +except: + print("SKIP") + raise SystemExit + +if sys.platform in ("esp32", "esp8266", "nrf"): + # Software timers aren't implemented on the esp32 and esp8266 ports. + # The nrf port doesn't support selection of hard and soft callbacks, + # and only allows Timer(period=N), not Timer(freq=N). + print("SKIP") + raise SystemExit +else: + timer_id = -1 + +# Test both hard and soft IRQ handlers and both one-shot and periodic +# timers. We adjust period in tests/extmod/machine_soft_timer.py, so try +# adjusting freq here instead. The heap should be locked in hard callbacks +# and unlocked in soft callbacks. + + +def callback(t): + print("callback", mode[1], kind[1], freq, end=" ") + try: + allocate = bytearray(1) + print("unlocked") + except MemoryError: + print("locked") + + +modes = [(Timer.ONE_SHOT, "one-shot"), (Timer.PERIODIC, "periodic")] +kinds = [(False, "soft"), (True, "hard")] + +for mode in modes: + for kind in kinds: + for freq in 50, 25: + timer = Timer( + timer_id, + mode=mode[0], + freq=freq, + hard=kind[0], + callback=callback, + ) + sleep_ms(90) + timer.deinit() diff --git a/tests/extmod/machine_timer.py.exp b/tests/extmod/machine_timer.py.exp new file mode 100644 index 00000000000..26cdc644fdd --- /dev/null +++ b/tests/extmod/machine_timer.py.exp @@ -0,0 +1,16 @@ +callback one-shot soft 50 unlocked +callback one-shot soft 25 unlocked +callback one-shot hard 50 locked +callback one-shot hard 25 locked +callback periodic soft 50 unlocked +callback periodic soft 50 unlocked +callback periodic soft 50 unlocked +callback periodic soft 50 unlocked +callback periodic soft 25 unlocked +callback periodic soft 25 unlocked +callback periodic hard 50 locked +callback periodic hard 50 locked +callback periodic hard 50 locked +callback periodic hard 50 locked +callback periodic hard 25 locked +callback periodic hard 25 locked diff --git a/tests/extmod/machine_uart_irq_txidle.py b/tests/extmod/machine_uart_irq_txidle.py index feb95fabaa4..a56f6e3cf85 100644 --- a/tests/extmod/machine_uart_irq_txidle.py +++ b/tests/extmod/machine_uart_irq_txidle.py @@ -10,29 +10,7 @@ raise SystemExit import time, sys - -# Configure pins based on the target. -if "rp2" in sys.platform: - uart_id = 0 - tx_pin = "GPIO0" - rx_pin = "GPIO1" -elif "samd" in sys.platform and "ItsyBitsy M0" in sys.implementation._machine: - uart_id = 0 - tx_pin = "D1" - rx_pin = "D0" -elif "samd" in sys.platform and "ItsyBitsy M4" in sys.implementation._machine: - uart_id = 3 - tx_pin = "D1" - rx_pin = "D0" -elif "mimxrt" in sys.platform: - uart_id = 1 - tx_pin = None -elif "nrf" in sys.platform: - uart_id = 0 - tx_pin = None -else: - print("Please add support for this test on this platform.") - raise SystemExit +from target_wiring import uart_loopback_args, uart_loopback_kwargs def irq(u): @@ -43,11 +21,7 @@ def irq(u): # Test that the IRQ is called after the write has completed. for bits_per_s in (2400, 9600, 115200): - if tx_pin is None: - uart = UART(uart_id, bits_per_s) - else: - uart = UART(uart_id, bits_per_s, tx=tx_pin, rx=rx_pin) - + uart = UART(*uart_loopback_args, baudrate=bits_per_s, **uart_loopback_kwargs) uart.irq(irq, uart.IRQ_TXIDLE) # The IRQ_TXIDLE shall trigger after the message has been sent. Thus diff --git a/tests/extmod/machine_uart_tx.py b/tests/extmod/machine_uart_tx.py index e9652f3c7ac..4e0ff37a8b1 100644 --- a/tests/extmod/machine_uart_tx.py +++ b/tests/extmod/machine_uart_tx.py @@ -8,43 +8,37 @@ raise SystemExit import time, sys +from target_wiring import uart_loopback_args, uart_loopback_kwargs initial_delay_ms = 0 bit_margin = 0 timing_margin_us = 100 -# Configure pins based on the target. -if "esp32" in sys.platform: - uart_id = 1 - pins = {} +# Tune test parameters based on the target. +if "alif" in sys.platform: + bit_margin = 1 +elif "esp32" in sys.platform: timing_margin_us = 400 +elif "esp8266" in sys.platform: + timing_margin_us = 4100 elif "mimxrt" in sys.platform: - uart_id = 1 - pins = {} initial_delay_ms = 20 # UART sends idle frame after init, so wait for that bit_margin = 1 +elif "nrf" in sys.platform: + timing_margin_us = 130 elif "pyboard" in sys.platform: - uart_id = 4 - pins = {} initial_delay_ms = 50 # UART sends idle frame after init, so wait for that bit_margin = 1 # first start-bit must wait to sync with the UART clock elif "rp2" in sys.platform: - uart_id = 0 - pins = {"tx": "GPIO0", "rx": "GPIO1"} timing_margin_us = 180 elif "samd" in sys.platform: - uart_id = 2 - pins = {"tx": "D1", "rx": "D0"} timing_margin_us = 300 bit_margin = 1 -else: - print("SKIP") - raise SystemExit # Test that write+flush takes the expected amount of time to execute. for bits_per_s in (2400, 9600, 115200): - text = "Hello World" - uart = UART(uart_id, bits_per_s, bits=8, parity=None, stop=1, **pins) + text = "Hello World from MicroPython" + uart = UART(*uart_loopback_args, baudrate=bits_per_s, **uart_loopback_kwargs) time.sleep_ms(initial_delay_ms) start_us = time.ticks_us() diff --git a/tests/extmod/marshal_basic.py b/tests/extmod/marshal_basic.py new file mode 100644 index 00000000000..9e7b70be482 --- /dev/null +++ b/tests/extmod/marshal_basic.py @@ -0,0 +1,38 @@ +# Test the marshal module, basic functionality. + +try: + import marshal + + (lambda: 0).__code__ +except (AttributeError, ImportError): + print("SKIP") + raise SystemExit + +ftype = type(lambda: 0) + +# Test basic dumps and loads. +print(ftype(marshal.loads(marshal.dumps((lambda: a).__code__)), {"a": 4})()) + +# Test dumps of a result from compile(). +ftype(marshal.loads(marshal.dumps(compile("print(a)", "", "exec"))), {"print": print, "a": 5})() + +# Test marshalling a function with arguments. +print(ftype(marshal.loads(marshal.dumps((lambda x, y: x + y).__code__)), {})(1, 2)) + +# Test marshalling a function with default arguments. +print(ftype(marshal.loads(marshal.dumps((lambda x=0: x).__code__)), {})("arg")) + +# Test marshalling a function containing constant objects (a tuple). +print(ftype(marshal.loads(marshal.dumps((lambda: (None, ...)).__code__)), {})()) + +# Test instantiating multiple code's with different globals dicts. +code = marshal.loads(marshal.dumps((lambda: a).__code__)) +f1 = ftype(code, {"a": 1}) +f2 = ftype(code, {"a": 2}) +print(f1(), f2()) + +# Test unmarshallable object. +try: + marshal.dumps(type) +except ValueError: + print("ValueError") diff --git a/tests/extmod/marshal_fun_nested.py b/tests/extmod/marshal_fun_nested.py new file mode 100644 index 00000000000..fcf8f9a0fa4 --- /dev/null +++ b/tests/extmod/marshal_fun_nested.py @@ -0,0 +1,79 @@ +# Test the marshal module, with functions that have children. + +try: + import marshal + + (lambda: 0).__code__ +except (AttributeError, ImportError): + print("SKIP") + raise SystemExit + + +def f_with_child(): + def child(): + return a + + return child + + +def f_with_child_defargs(): + def child(a="default"): + return a + + return child + + +def f_with_child_closure(): + a = "closure 1" + + def child(): + return a + + a = "closure 2" + return child + + +def f_with_child_closure_defargs(): + a = "closure defargs 1" + + def child(b="defargs default"): + return (a, b) + + a = "closure defargs 1" + return child + + +def f_with_list_comprehension(a): + return [i + a for i in range(4)] + + +ftype = type(lambda: 0) + +# Test function with a child. +f = ftype(marshal.loads(marshal.dumps(f_with_child.__code__)), {"a": "global"}) +print(f()()) + +# Test function with a child that has default arguments. +f = ftype(marshal.loads(marshal.dumps(f_with_child_defargs.__code__)), {}) +print(f()()) +print(f()("non-default")) + +# Test function with a child that is a closure. +f = ftype(marshal.loads(marshal.dumps(f_with_child_closure.__code__)), {}) +print(f()()) + +# Test function with a child that is a closure and has default arguments. +f = ftype(marshal.loads(marshal.dumps(f_with_child_closure_defargs.__code__)), {}) +print(f()()) +print(f()("defargs non-default")) + +# Test function with a list comprehension (which will be an anonymous child). +f = ftype(marshal.loads(marshal.dumps(f_with_list_comprehension.__code__)), {}) +print(f(10)) + +# Test child within a module (the outer scope). +code = compile("def child(a): return a", "", "exec") +f = marshal.loads(marshal.dumps(code)) +ctx = {} +exec(f, ctx) +print(ctx["child"]("arg")) diff --git a/tests/extmod/marshal_stress.py b/tests/extmod/marshal_stress.py new file mode 100644 index 00000000000..b52475c0361 --- /dev/null +++ b/tests/extmod/marshal_stress.py @@ -0,0 +1,122 @@ +# Test the marshal module, stressing edge cases. + +try: + import marshal + + (lambda: 0).__code__ +except (AttributeError, ImportError): + print("SKIP") + raise SystemExit + +ftype = type(lambda: 0) + +# Test a large function. + + +def large_function(arg0, arg1, arg2, arg3): + # Arguments. + print(arg0, arg1, arg2, arg3) + + # Positive medium-sized integer (still a small-int though). + print(1234) + + # Negative small-ish integer. + print(-20) + + # More than 64 constant objects. + x = (0,) + x = (1,) + x = (2,) + x = (3,) + x = (4,) + x = (5,) + x = (6,) + x = (7,) + x = (8,) + x = (9,) + x = (10,) + x = (11,) + x = (12,) + x = (13,) + x = (14,) + x = (15,) + x = (16,) + x = (17,) + x = (18,) + x = (19,) + x = (20,) + x = (21,) + x = (22,) + x = (23,) + x = (24,) + x = (25,) + x = (26,) + x = (27,) + x = (28,) + x = (29,) + x = (30,) + x = (31,) + x = (32,) + x = (33,) + x = (34,) + x = (35,) + x = (36,) + x = (37,) + x = (38,) + x = (39,) + x = (40,) + x = (41,) + x = (42,) + x = (43,) + x = (44,) + x = (45,) + x = (46,) + x = (47,) + x = (48,) + x = (49,) + x = (50,) + x = (51,) + x = (52,) + x = (53,) + x = (54,) + x = (55,) + x = (56,) + x = (57,) + x = (58,) + x = (59,) + x = (60,) + x = (61,) + x = (62,) + x = (63,) + x = (64,) + + # Small jump. + x = 0 + while x < 2: + print("loop", x) + x += 1 + + # Large jump. + x = 0 + while x < 2: + try: + try: + try: + print + except Exception as e: + print + finally: + print + except Exception as e: + print + finally: + print + except Exception as e: + print + finally: + print("loop", x) + x += 1 + + +code = marshal.dumps(large_function.__code__) +ftype(marshal.loads(code), {"print": print})(0, 1, 2, 3) diff --git a/tests/extmod/os_urandom.py b/tests/extmod/os_urandom.py new file mode 100644 index 00000000000..982d6d04b96 --- /dev/null +++ b/tests/extmod/os_urandom.py @@ -0,0 +1,14 @@ +# Test os.urandom(). + +try: + from os import urandom +except ImportError: + print("SKIP") + raise SystemExit + +for n in range(-2, 5, 1): + try: + r = urandom(n) + print(n, type(r), len(r)) + except ValueError: + print(n, "ValueError") diff --git a/tests/extmod/platform_basic.py b/tests/extmod/platform_basic.py new file mode 100644 index 00000000000..eb6f2be13c1 --- /dev/null +++ b/tests/extmod/platform_basic.py @@ -0,0 +1,8 @@ +try: + import platform +except ImportError: + print("SKIP") + raise SystemExit + +print(type(platform.python_compiler())) +print(type(platform.libc_ver())) diff --git a/tests/extmod/random_extra_float.py b/tests/extmod/random_extra_float.py index 3b37ed8dcef..03973c58349 100644 --- a/tests/extmod/random_extra_float.py +++ b/tests/extmod/random_extra_float.py @@ -1,12 +1,8 @@ try: import random -except ImportError: - print("SKIP") - raise SystemExit -try: - random.randint -except AttributeError: + random.random +except (ImportError, AttributeError): print("SKIP") raise SystemExit diff --git a/tests/extmod/re_error.py b/tests/extmod/re_error.py index f61d0913289..bd678c9d251 100644 --- a/tests/extmod/re_error.py +++ b/tests/extmod/re_error.py @@ -11,7 +11,7 @@ def test_re(r): try: re.compile(r) print("OK") - except: # uPy and CPy use different errors, so just ignore the type + except: # MPy and CPy use different errors, so just ignore the type print("Error") diff --git a/tests/extmod/re_stack_overflow2.py b/tests/extmod/re_stack_overflow2.py new file mode 100644 index 00000000000..c5ddd012b32 --- /dev/null +++ b/tests/extmod/re_stack_overflow2.py @@ -0,0 +1,25 @@ +# Test overflow in re.compile output code. + +try: + import re +except ImportError: + print("SKIP") + raise SystemExit + + +def test_re(r): + try: + re.compile(r) + except: + print("Error") + + +try: + r = "(" * 65536 + ")" * 65536 +except MemoryError: + print("SKIP") + raise SystemExit + +# This happens to trigger RecursionError on current versions of CPython +# (tested with 3.13.5) as well, so no .exp file is needed. +test_re(r) diff --git a/tests/extmod/re_start_end_pos.py b/tests/extmod/re_start_end_pos.py new file mode 100644 index 00000000000..bd16584374b --- /dev/null +++ b/tests/extmod/re_start_end_pos.py @@ -0,0 +1,78 @@ +# test start and end pos specification + +try: + import re +except ImportError: + print("SKIP") + raise SystemExit + + +def print_groups(match): + print("----") + try: + if match is not None: + i = 0 + while True: + print(match.group(i)) + i += 1 + except IndexError: + pass + + +p = re.compile(r"o") +m = p.match("dog") +print_groups(m) + +m = p.match("dog", 1) +print_groups(m) + +m = p.match("dog", 2) +print_groups(m) + +# No match past end of input +m = p.match("dog", 5) +print_groups(m) + +m = p.match("dog", 0, 1) +print_groups(m) + +# Caret only matches the actual beginning +p = re.compile(r"^o") +m = p.match("dog", 1) +print_groups(m) + +# End at beginning means searching empty string +p = re.compile(r"o") +m = p.match("dog", 1, 1) +print_groups(m) + +# End before the beginning doesn't match anything +m = p.match("dog", 2, 1) +print_groups(m) + +# Negative starting values don't crash +m = p.search("dog", -2) +print_groups(m) + +m = p.search("dog", -2, -5) +print_groups(m) + +# Search also works +print("--search") + +p = re.compile(r"o") +m = p.search("dog") +print_groups(m) + +m = p.search("dog", 1) +print_groups(m) + +m = p.search("dog", 2) +print_groups(m) + +# Negative starting values don't crash +m = p.search("dog", -2) +print_groups(m) + +m = p.search("dog", -2, -5) +print_groups(m) diff --git a/tests/extmod/re_sub.py b/tests/extmod/re_sub.py index 2c7c6c10f1a..95ba1d70d6e 100644 --- a/tests/extmod/re_sub.py +++ b/tests/extmod/re_sub.py @@ -10,6 +10,8 @@ print("SKIP") raise SystemExit +import sys + def multiply(m): return str(int(m.group(0)) * 2) @@ -47,7 +49,11 @@ def A(): print(re.sub("a", "b", "c")) # with maximum substitution count specified -print(re.sub("a", "b", "1a2a3a", 2)) +if sys.implementation.name != "micropython": + # On CPython 3.13 and later the substitution count must be a keyword argument. + print(re.sub("a", "b", "1a2a3a", count=2)) +else: + print(re.sub("a", "b", "1a2a3a", 2)) # invalid group try: @@ -55,7 +61,7 @@ def A(): except: print("invalid group") -# invalid group with very large number (to test overflow in uPy) +# invalid group with very large number (to test overflow in MicroPython) try: re.sub("(a)", "b\\199999999999999999999999999999999999999", "a") except: diff --git a/tests/extmod/re_sub_unmatched.py.exp b/tests/extmod/re_sub_unmatched.py.exp deleted file mode 100644 index 1e5f0fda055..00000000000 --- a/tests/extmod/re_sub_unmatched.py.exp +++ /dev/null @@ -1 +0,0 @@ -1-a2 diff --git a/tests/extmod/select_poll_custom.py b/tests/extmod/select_poll_custom.py index b854a8a14da..0d2ab07b232 100644 --- a/tests/extmod/select_poll_custom.py +++ b/tests/extmod/select_poll_custom.py @@ -1,13 +1,13 @@ # Test custom pollable objects implemented in Python. -from micropython import const - try: import socket, select, io except ImportError: print("SKIP") raise SystemExit +from micropython import const + _MP_STREAM_POLL = const(3) _MP_STREAM_GET_FILENO = const(10) diff --git a/tests/extmod/select_poll_eintr.py b/tests/extmod/select_poll_eintr.py index e1cbc2aaf57..fdc5ee5074a 100644 --- a/tests/extmod/select_poll_eintr.py +++ b/tests/extmod/select_poll_eintr.py @@ -10,6 +10,18 @@ print("SKIP") raise SystemExit +# Use a new UDP socket for tests, which should be writable but not readable. +s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +localhost_addr_info = socket.getaddrinfo("127.0.0.1", 8000) +try: + s.bind(localhost_addr_info[0][-1]) +except OSError: + # Target can't bind to localhost. + # Most likely it doesn't have a NIC and the test cannot be run. + s.close() + print("SKIP") + raise SystemExit + def thread_main(): lock.acquire() @@ -21,15 +33,19 @@ def thread_main(): print("thread gc end") +# Pre-allocate global variables here so the global dict is not resized by the main +# thread while the secondary thread runs. This is a workaround for the bug described +# in https://github.com/micropython/micropython/pull/11604 +poller = None +t0 = None +result = None +dt_ms = None + # Start a thread to interrupt the main thread during its call to poll. lock = _thread.allocate_lock() lock.acquire() _thread.start_new_thread(thread_main, ()) -# Use a new UDP socket for tests, which should be writable but not readable. -s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) -s.bind(socket.getaddrinfo("127.0.0.1", 8000)[0][-1]) - # Create the poller object. poller = select.poll() poller.register(s, select.POLLIN) diff --git a/tests/extmod/select_poll_udp.py b/tests/extmod/select_poll_udp.py index 133871b1a42..887176a4f65 100644 --- a/tests/extmod/select_poll_udp.py +++ b/tests/extmod/select_poll_udp.py @@ -29,3 +29,5 @@ if hasattr(select, "select"): r, w, e = select.select([s], [], [], 0) assert not r and not w and not e + +s.close() diff --git a/tests/extmod/socket_badconstructor.py b/tests/extmod/socket_badconstructor.py new file mode 100644 index 00000000000..25d981f70a7 --- /dev/null +++ b/tests/extmod/socket_badconstructor.py @@ -0,0 +1,29 @@ +# Test passing in bad values to socket.socket constructor. + +try: + import socket +except: + print("SKIP") + raise SystemExit + +try: + s = socket.socket(None) +except TypeError: + print("TypeError") + +try: + s = socket.socket(socket.AF_INET, None) +except TypeError: + print("TypeError") + +# This may or may not raise an exception, depending on the socket implementation. +# The test is here for coverage. +try: + s = socket.socket(socket.AF_INET, 123456) +except OSError: + pass + +try: + s = socket.socket(socket.AF_INET, socket.SOCK_RAW, None) +except TypeError: + print("TypeError") diff --git a/tests/extmod/socket_fileno.py b/tests/extmod/socket_fileno.py new file mode 100644 index 00000000000..da15825e3d5 --- /dev/null +++ b/tests/extmod/socket_fileno.py @@ -0,0 +1,17 @@ +# Test socket.fileno() functionality + +try: + import socket +except ImportError: + print("SKIP") + raise SystemExit + +if not hasattr(socket.socket, "fileno"): + print("SKIP") + raise SystemExit + +s = socket.socket() +print(s.fileno() >= 0) + +s.close() +print(s.fileno()) # should print -1 diff --git a/tests/extmod/socket_udp_nonblock.py b/tests/extmod/socket_udp_nonblock.py index 1e74e2917dc..394115e4b88 100644 --- a/tests/extmod/socket_udp_nonblock.py +++ b/tests/extmod/socket_udp_nonblock.py @@ -19,3 +19,5 @@ s.recv(1) except OSError as er: print("EAGAIN:", er.errno == errno.EAGAIN) + +s.close() diff --git a/tests/extmod/time_mktime.py b/tests/extmod/time_mktime.py new file mode 100644 index 00000000000..7fc643dc3cb --- /dev/null +++ b/tests/extmod/time_mktime.py @@ -0,0 +1,120 @@ +# test conversion from date tuple to timestamp and back + +try: + import time + + time.localtime +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit + +# Range of date expected to work on all MicroPython platforms +MIN_YEAR = 1970 +MAX_YEAR = 2099 +# CPython properly supported date range: +# - on Windows: year 1970 to 3000+ +# - on Unix: year 1583 to 3000+ + +# Start test from Jan 1, 2001 13:00 (Feb 2000 might already be broken) +SAFE_DATE = (2001, 1, 1, 13, 0, 0, 0, 0, -1) + + +# mktime function that checks that the result is reversible +def safe_mktime(date_tuple): + try: + res = time.mktime(date_tuple) + chk = time.localtime(res) + except OverflowError: + print("safe_mktime:", date_tuple, "overflow error") + return None + if chk[0:5] != date_tuple[0:5]: + print("safe_mktime:", date_tuple[0:5], " -> ", res, " -> ", chk[0:5]) + return None + return res + + +# localtime function that checks that the result is reversible +def safe_localtime(timestamp): + try: + res = time.localtime(timestamp) + chk = time.mktime(res) + except OverflowError: + print("safe_localtime:", timestamp, "overflow error") + return None + if chk != timestamp: + print("safe_localtime:", timestamp, " -> ", res, " -> ", chk) + return None + return res + + +# look for smallest valid timestamps by iterating backwards on tuple +def test_bwd(date_tuple): + curr_stamp = safe_mktime(date_tuple) + year = date_tuple[0] + month = date_tuple[1] - 1 + if month < 1: + year -= 1 + month = 12 + while year >= MIN_YEAR: + while month >= 1: + next_tuple = (year, month) + date_tuple[2:] + next_stamp = safe_mktime(next_tuple) + # at this stage, only test consistency and monotonicity + if next_stamp is None or next_stamp >= curr_stamp: + return date_tuple + date_tuple = next_tuple + curr_stamp = next_stamp + month -= 1 + year -= 1 + month = 12 + return date_tuple + + +# test day-by-day to ensure that every date is properly converted +def test_fwd(start_date): + DAYS_PER_MONTH = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + curr_stamp = safe_mktime(start_date) + curr_date = safe_localtime(curr_stamp) + while curr_date[0] <= MAX_YEAR: + if curr_date[2] < 15: + skip_days = 13 + else: + skip_days = 1 + next_stamp = curr_stamp + skip_days * 86400 + next_date = safe_localtime(next_stamp) + if next_date is None: + return curr_date + if next_date[2] != curr_date[2] + skip_days: + # next month + if next_date[2] != 1: + print("wrong day of month:", next_date) + return curr_date + # check the number of days in previous month + month_days = DAYS_PER_MONTH[curr_date[1]] + if month_days == 28 and curr_date[0] % 4 == 0: + if curr_date[0] % 100 != 0 or curr_date[0] % 400 == 0: + month_days += 1 + if curr_date[2] != month_days: + print("wrong day count in prev month:", curr_date[2], "vs", month_days) + return curr_date + if next_date[1] != curr_date[1] + 1: + # next year + if curr_date[1] != 12: + print("wrong month count in prev year:", curr_date[1]) + return curr_date + if next_date[1] != 1: + print("wrong month:", next_date) + return curr_date + if next_date[0] != curr_date[0] + 1: + print("wrong year:", next_date) + return curr_date + curr_stamp = next_stamp + curr_date = next_date + return curr_date + + +small_date = test_bwd(SAFE_DATE) +large_date = test_fwd(small_date) +print("tested from", small_date[0:3], "to", large_date[0:3]) +print(small_date[0:3], "wday is", small_date[6]) +print(large_date[0:3], "wday is", large_date[6]) diff --git a/tests/extmod/time_res.py b/tests/extmod/time_res.py index 548bef1f174..ef20050b914 100644 --- a/tests/extmod/time_res.py +++ b/tests/extmod/time_res.py @@ -37,9 +37,12 @@ def test(): time.sleep_ms(100) for func_name, _ in EXPECTED_MAP: try: - time_func = getattr(time, func_name, None) or globals()[func_name] + if func_name.endswith("_time"): + time_func = globals()[func_name] + else: + time_func = getattr(time, func_name) now = time_func() # may raise AttributeError - except (KeyError, AttributeError): + except AttributeError: continue try: results_map[func_name].add(now) diff --git a/tests/extmod/tls_dtls.py b/tests/extmod/tls_dtls.py new file mode 100644 index 00000000000..753ab2fee4f --- /dev/null +++ b/tests/extmod/tls_dtls.py @@ -0,0 +1,61 @@ +# Test DTLS functionality including timeout handling + +try: + from tls import PROTOCOL_DTLS_CLIENT, PROTOCOL_DTLS_SERVER, SSLContext, CERT_NONE + import io +except ImportError: + print("SKIP") + raise SystemExit + + +class DummySocket(io.IOBase): + def __init__(self): + self.write_buffer = bytearray() + self.read_buffer = bytearray() + + def write(self, data): + return len(data) + + def readinto(self, buf): + # This is a placeholder socket that doesn't actually read anything + # so the read buffer is always empty. + return None + + def ioctl(self, req, arg): + if req == 4: # MP_STREAM_CLOSE + return 0 + return -1 + + +# Create dummy sockets for testing +server_socket = DummySocket() +client_socket = DummySocket() + +# Wrap the DTLS Server +dtls_server_ctx = SSLContext(PROTOCOL_DTLS_SERVER) +dtls_server_ctx.verify_mode = CERT_NONE +dtls_server = dtls_server_ctx.wrap_socket( + server_socket, do_handshake_on_connect=False, client_id=b"dummy_client_id" +) +print("Wrapped DTLS Server") + +# wrap DTLS server with invalid client_id +try: + dtls_server = dtls_server_ctx.wrap_socket( + server_socket, do_handshake_on_connect=False, client_id=4 + ) +except OSError: + print("Failed to wrap DTLS Server with invalid client_id") + +# Wrap the DTLS Client +dtls_client_ctx = SSLContext(PROTOCOL_DTLS_CLIENT) +dtls_client_ctx.verify_mode = CERT_NONE +dtls_client = dtls_client_ctx.wrap_socket(client_socket, do_handshake_on_connect=False) +print("Wrapped DTLS Client") + +# Trigger the timing check multiple times with different elapsed times +for i in range(10): # Try multiple iterations to hit the timing window + dtls_client.write(b"test") + data = dtls_server.read(1024) # This should eventually hit the timing condition + +print("OK") diff --git a/tests/extmod/tls_dtls.py.exp b/tests/extmod/tls_dtls.py.exp new file mode 100644 index 00000000000..dbd005d0edf --- /dev/null +++ b/tests/extmod/tls_dtls.py.exp @@ -0,0 +1,4 @@ +Wrapped DTLS Server +Failed to wrap DTLS Server with invalid client_id +Wrapped DTLS Client +OK diff --git a/tests/extmod/tls_noleak.py b/tests/extmod/tls_noleak.py new file mode 100644 index 00000000000..870032d58e6 --- /dev/null +++ b/tests/extmod/tls_noleak.py @@ -0,0 +1,50 @@ +# Ensure that SSLSockets can be allocated sequentially +# without running out of available memory. +try: + import io + import tls +except ImportError: + print("SKIP") + raise SystemExit + +import unittest + + +class TestSocket(io.IOBase): + def write(self, buf): + return len(buf) + + def readinto(self, buf): + return 0 + + def ioctl(self, cmd, arg): + return 0 + + def setblocking(self, value): + pass + + +ITERS = 128 + + +class TLSNoLeaks(unittest.TestCase): + def test_unique_context(self): + for n in range(ITERS): + print(n) + s = TestSocket() + ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT) + ctx.verify_mode = tls.CERT_NONE + s = ctx.wrap_socket(s, do_handshake_on_connect=False) + + def test_shared_context(self): + # Single SSLContext, multiple sockets + ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT) + ctx.verify_mode = tls.CERT_NONE + for n in range(ITERS): + print(n) + s = TestSocket() + s = ctx.wrap_socket(s, do_handshake_on_connect=False) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/extmod/tls_threads.py b/tests/extmod/tls_threads.py new file mode 100644 index 00000000000..1e0c3d23d2f --- /dev/null +++ b/tests/extmod/tls_threads.py @@ -0,0 +1,58 @@ +# Ensure that SSL sockets can be allocated from multiple +# threads without thread safety issues + +try: + import _thread + import io + import tls + import time +except ImportError: + print("SKIP") + raise SystemExit + +import unittest + + +class TestSocket(io.IOBase): + def write(self, buf): + return len(buf) + + def readinto(self, buf): + return 0 + + def ioctl(self, cmd, arg): + return 0 + + def setblocking(self, value): + pass + + +ITERS = 256 + + +class TLSThreads(unittest.TestCase): + def test_sslsocket_threaded(self): + self.done = False + # only run in two threads: too much RAM demand otherwise, and rp2 only + # supports two anyhow + _thread.start_new_thread(self._alloc_many_sockets, (True,)) + self._alloc_many_sockets(False) + while not self.done: + time.sleep(0.1) + print("done") + + def _alloc_many_sockets(self, set_done_flag): + print("start", _thread.get_ident()) + ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT) + ctx.verify_mode = tls.CERT_NONE + for n in range(ITERS): + s = TestSocket() + s = ctx.wrap_socket(s, do_handshake_on_connect=False) + s.close() # Free associated resources now from thread, not in a GC pass + print("done", _thread.get_ident()) + if set_done_flag: + self.done = True + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/extmod/uctypes_addressof.py b/tests/extmod/uctypes_addressof.py new file mode 100644 index 00000000000..213fcc05eee --- /dev/null +++ b/tests/extmod/uctypes_addressof.py @@ -0,0 +1,19 @@ +# Test uctypes.addressof(). + +try: + from sys import maxsize + import uctypes +except ImportError: + print("SKIP") + raise SystemExit + +# Test small addresses. +for i in range(8): + print(uctypes.addressof(uctypes.bytearray_at(1 << i, 8))) + +# Test address that is bigger than the greatest small-int but still within the address range. +try: + large_addr = maxsize + 1 + print(uctypes.addressof(uctypes.bytearray_at(large_addr, 8)) == large_addr) +except OverflowError: + print(True) # systems with 64-bit bigints will overflow on the above operation diff --git a/tests/extmod/uctypes_addressof.py.exp b/tests/extmod/uctypes_addressof.py.exp new file mode 100644 index 00000000000..5b48569d018 --- /dev/null +++ b/tests/extmod/uctypes_addressof.py.exp @@ -0,0 +1,9 @@ +1 +2 +4 +8 +16 +32 +64 +128 +True diff --git a/tests/extmod/uctypes_array_load_store.py b/tests/extmod/uctypes_array_load_store.py index 3b9bb6d7308..df7deb6837a 100644 --- a/tests/extmod/uctypes_array_load_store.py +++ b/tests/extmod/uctypes_array_load_store.py @@ -6,6 +6,13 @@ print("SKIP") raise SystemExit +# 'int' needs to be able to represent UINT64 for this test +try: + int("FF" * 8, 16) +except OverflowError: + print("SKIP") + raise SystemExit + N = 5 for endian in ("NATIVE", "LITTLE_ENDIAN", "BIG_ENDIAN"): diff --git a/tests/extmod/vfs_basic.py b/tests/extmod/vfs_basic.py index 2c0ce8f5295..09fe5dce022 100644 --- a/tests/extmod/vfs_basic.py +++ b/tests/extmod/vfs_basic.py @@ -19,11 +19,11 @@ def umount(self): print(self.id, "umount") def ilistdir(self, dir): - print(self.id, "ilistdir", dir) + print(self.id, "ilistdir", repr(dir)) return iter([("a%d" % self.id, 0, 0)]) def chdir(self, dir): - print(self.id, "chdir", dir) + print(self.id, "chdir", repr(dir)) if self.fail: raise OSError(self.fail) @@ -32,23 +32,23 @@ def getcwd(self): return "dir%d" % self.id def mkdir(self, path): - print(self.id, "mkdir", path) + print(self.id, "mkdir", repr(path)) def remove(self, path): - print(self.id, "remove", path) + print(self.id, "remove", repr(path)) def rename(self, old_path, new_path): - print(self.id, "rename", old_path, new_path) + print(self.id, "rename", repr(old_path), repr(new_path)) def rmdir(self, path): - print(self.id, "rmdir", path) + print(self.id, "rmdir", repr(path)) def stat(self, path): - print(self.id, "stat", path) + print(self.id, "stat", repr(path)) return (self.id,) def statvfs(self, path): - print(self.id, "statvfs", path) + print(self.id, "statvfs", repr(path)) return (self.id,) def open(self, file, mode): diff --git a/tests/extmod/vfs_basic.py.exp b/tests/extmod/vfs_basic.py.exp index 536bb4c805d..a03176a3c44 100644 --- a/tests/extmod/vfs_basic.py.exp +++ b/tests/extmod/vfs_basic.py.exp @@ -18,30 +18,30 @@ stat /x OSError ('test_mnt', 16384, 0) StopIteration StopIteration -1 ilistdir / +1 ilistdir '/' ['a1'] -1 ilistdir / +1 ilistdir '/' ['a1'] 2 mount True False ['test_mnt', 'test_mnt2'] -2 ilistdir / +2 ilistdir '/' ['a2'] 3 mount False False OSError OSError OSError -1 chdir / -1 ilistdir +1 chdir '/' +1 ilistdir '' ['a1'] 1 getcwd /test_mntdir1 -1 mkdir test_dir -1 remove test_file -1 rename test_file test_file2 -1 rmdir test_dir -1 stat test_file +1 mkdir 'test_dir' +1 remove 'test_file' +1 rename 'test_file' 'test_file2' +1 rmdir 'test_dir' +1 stat 'test_file' (1,) -1 statvfs / +1 statvfs '/' (1,) 1 open test_file r 1 open test_file wb @@ -50,29 +50,29 @@ OSError OSError 3 mount False False (16384, 0, 0, 0, 0, 0, 0, 0, 0, 0) -3 statvfs / +3 statvfs '/' (3,) -3 ilistdir / +3 ilistdir '/' ['a3'] 3 open test r 4 mount False False -3 ilistdir / +3 ilistdir '/' ['mnt', 'a3'] -4 ilistdir / +4 ilistdir '/' ['a4'] -4 chdir / -4 ilistdir +4 chdir '/' +4 ilistdir '' ['a4'] -3 chdir /subdir -3 ilistdir +3 chdir '/subdir' +3 ilistdir '' ['a3'] -3 chdir / +3 chdir '/' 3 umount ['mnt'] 4 umount OSError / 5 mount False False -5 chdir /subdir +5 chdir '/subdir' OSError / diff --git a/tests/extmod/vfs_blockdev_invalid.py b/tests/extmod/vfs_blockdev_invalid.py index 4d00f4b0027..955f8495b3f 100644 --- a/tests/extmod/vfs_blockdev_invalid.py +++ b/tests/extmod/vfs_blockdev_invalid.py @@ -46,12 +46,16 @@ def ioctl(self, op, arg): try: bdev = RAMBlockDevice(50) except MemoryError: - print("SKIP") + print("SKIP-TOO-LARGE") raise SystemExit -def test(vfs_class): - print(vfs_class) +ERROR_EIO = (OSError, "[Errno 5] EIO") +ERROR_EINVAL = (OSError, "[Errno 22] EINVAL") +ERROR_TYPE = (TypeError, "can't convert str to int") + + +def test(vfs_class, test_data): bdev.read_res = 0 # reset function results bdev.write_res = 0 @@ -61,17 +65,15 @@ def test(vfs_class): with fs.open("test", "w") as f: f.write("a" * 64) - for res in (0, -5, 5, 33, "invalid"): - # -5 is a legitimate negative failure (EIO), positive integer - # is not - + for res, error_open, error_read in test_data: # This variant will fail on open bdev.read_res = res try: with fs.open("test", "r") as f: - print("opened") - except OSError as e: - print("OSError", e) + assert error_open is None + except Exception as e: + assert error_open is not None + assert (type(e), str(e)) == error_open # This variant should succeed on open, may fail on read # unless the filesystem cached the contents already @@ -79,11 +81,35 @@ def test(vfs_class): try: with fs.open("test", "r") as f: bdev.read_res = res - print("read 1", f.read(1)) - print("read rest", f.read()) - except OSError as e: - print("OSError", e) + assert f.read(1) == "a" + assert f.read() == "a" * 63 + assert error_read is None + except Exception as e: + assert error_read is not None + assert (type(e), str(e)) == error_read -test(vfs.VfsLfs2) -test(vfs.VfsFat) +try: + test( + vfs.VfsLfs2, + ( + (0, None, None), + (-5, ERROR_EIO, None), + (5, ERROR_EINVAL, None), + (33, ERROR_EINVAL, None), + ("invalid", ERROR_TYPE, None), + ), + ) + test( + vfs.VfsFat, + ( + (0, None, None), + (-5, ERROR_EIO, ERROR_EIO), + (5, ERROR_EIO, ERROR_EIO), + (33, ERROR_EIO, ERROR_EIO), + ("invalid", ERROR_TYPE, ERROR_TYPE), + ), + ) + print("OK") +except MemoryError: + print("SKIP-TOO-LARGE") diff --git a/tests/extmod/vfs_blockdev_invalid.py.exp b/tests/extmod/vfs_blockdev_invalid.py.exp index 13695e0d889..d86bac9de59 100644 --- a/tests/extmod/vfs_blockdev_invalid.py.exp +++ b/tests/extmod/vfs_blockdev_invalid.py.exp @@ -1,28 +1 @@ - -opened -read 1 a -read rest aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -OSError [Errno 5] EIO -read 1 a -read rest aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -OSError [Errno 22] EINVAL -read 1 a -read rest aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -OSError [Errno 22] EINVAL -read 1 a -read rest aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -OSError [Errno 22] EINVAL -read 1 a -read rest aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - -opened -read 1 a -read rest aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -OSError [Errno 5] EIO -OSError [Errno 5] EIO -OSError [Errno 5] EIO -OSError [Errno 5] EIO -OSError [Errno 5] EIO -OSError [Errno 5] EIO -OSError [Errno 5] EIO -OSError [Errno 5] EIO +OK diff --git a/tests/extmod/vfs_blockdev_invalid2.py b/tests/extmod/vfs_blockdev_invalid2.py new file mode 100644 index 00000000000..8905f833cf7 --- /dev/null +++ b/tests/extmod/vfs_blockdev_invalid2.py @@ -0,0 +1,37 @@ +# Tests where the block device returns invalid values + +try: + import vfs + + vfs.VfsFat + memoryview +except (NameError, ImportError, AttributeError): + print("SKIP") + raise SystemExit + + +class BadDev: + SEC_SIZE = 512 + + def __init__(self, blocks): + self.blocks = blocks + + def readblocks(self, n, buf): + assert len(buf) == self.SEC_SIZE + buf[:] = bytearray(self.SEC_SIZE + 1) # Attempts to enlarge passed-in buf + + def writeblocks(self, n, buf): + pass + + def ioctl(self, op, arg): + if op == 4: # MP_BLOCKDEV_IOCTL_BLOCK_COUNT + return self.blocks + if op == 5: # MP_BLOCKDEV_IOCTL_BLOCK_SIZE + return self.SEC_SIZE + + +bdev = BadDev(512) +try: + vfs.VfsFat.mkfs(bdev) +except ValueError as e: + print("ValueError") diff --git a/tests/extmod/vfs_blockdev_invalid2.py.exp b/tests/extmod/vfs_blockdev_invalid2.py.exp new file mode 100644 index 00000000000..94274de1bb3 --- /dev/null +++ b/tests/extmod/vfs_blockdev_invalid2.py.exp @@ -0,0 +1 @@ +ValueError diff --git a/tests/extmod/vfs_fat_ilistdir_del.py b/tests/extmod/vfs_fat_ilistdir_del.py index a6e24ec92f3..964e6b12868 100644 --- a/tests/extmod/vfs_fat_ilistdir_del.py +++ b/tests/extmod/vfs_fat_ilistdir_del.py @@ -1,8 +1,7 @@ # Test ilistdir __del__ for VfsFat using a RAM device. -import gc try: - import os, vfs + import gc, os, vfs vfs.VfsFat except (ImportError, AttributeError): diff --git a/tests/extmod/vfs_lfs.py b/tests/extmod/vfs_lfs.py index 3ad57fd9c38..40d58e9c9f7 100644 --- a/tests/extmod/vfs_lfs.py +++ b/tests/extmod/vfs_lfs.py @@ -136,7 +136,7 @@ def test(bdev, vfs_class): print(fs.getcwd()) fs.chdir("../testdir") print(fs.getcwd()) - fs.chdir("../..") + fs.chdir("..") print(fs.getcwd()) fs.chdir(".//testdir") print(fs.getcwd()) diff --git a/tests/extmod/vfs_lfs_error.py b/tests/extmod/vfs_lfs_error.py index 2ac7629bfa8..73cdf343733 100644 --- a/tests/extmod/vfs_lfs_error.py +++ b/tests/extmod/vfs_lfs_error.py @@ -1,7 +1,7 @@ # Test for VfsLittle using a RAM device, testing error handling try: - import vfs + import errno, vfs vfs.VfsLfs1 vfs.VfsLfs2 @@ -41,14 +41,14 @@ def test(bdev, vfs_class): # mkfs with too-small block device try: vfs_class.mkfs(RAMBlockDevice(1)) - except OSError: - print("mkfs OSError") + except OSError as er: + print("mkfs OSError", er.errno > 0) # mount with invalid filesystem try: vfs_class(bdev) - except OSError: - print("mount OSError") + except OSError as er: + print("mount OSError", er.errno > 0) # set up for following tests vfs_class.mkfs(bdev) @@ -60,60 +60,60 @@ def test(bdev, vfs_class): # ilistdir try: fs.ilistdir("noexist") - except OSError: - print("ilistdir OSError") + except OSError as er: + print("ilistdir OSError", er) # remove try: fs.remove("noexist") - except OSError: - print("remove OSError") + except OSError as er: + print("remove OSError", er) # rmdir try: fs.rmdir("noexist") - except OSError: - print("rmdir OSError") + except OSError as er: + print("rmdir OSError", er) # rename try: fs.rename("noexist", "somethingelse") - except OSError: - print("rename OSError") + except OSError as er: + print("rename OSError", er) # mkdir try: fs.mkdir("testdir") - except OSError: - print("mkdir OSError") + except OSError as er: + print("mkdir OSError", er) # chdir to nonexistent try: fs.chdir("noexist") - except OSError: - print("chdir OSError") + except OSError as er: + print("chdir OSError", er) print(fs.getcwd()) # check still at root # chdir to file try: fs.chdir("testfile") - except OSError: - print("chdir OSError") + except OSError as er: + print("chdir OSError", er) print(fs.getcwd()) # check still at root # stat try: fs.stat("noexist") - except OSError: - print("stat OSError") + except OSError as er: + print("stat OSError", er) # error during seek with fs.open("testfile", "r") as f: f.seek(1 << 30) # SEEK_SET try: f.seek(1 << 30, 1) # SEEK_CUR - except OSError: - print("seek OSError") + except OSError as er: + print("seek OSError", er) bdev = RAMBlockDevice(30) diff --git a/tests/extmod/vfs_lfs_error.py.exp b/tests/extmod/vfs_lfs_error.py.exp index f4327f6962e..440607ed84b 100644 --- a/tests/extmod/vfs_lfs_error.py.exp +++ b/tests/extmod/vfs_lfs_error.py.exp @@ -1,28 +1,28 @@ test -mkfs OSError -mount OSError -ilistdir OSError -remove OSError -rmdir OSError -rename OSError -mkdir OSError -chdir OSError +mkfs OSError True +mount OSError True +ilistdir OSError [Errno 2] ENOENT +remove OSError [Errno 2] ENOENT +rmdir OSError [Errno 2] ENOENT +rename OSError [Errno 2] ENOENT +mkdir OSError [Errno 17] EEXIST +chdir OSError [Errno 2] ENOENT / -chdir OSError +chdir OSError [Errno 2] ENOENT / -stat OSError -seek OSError +stat OSError [Errno 2] ENOENT +seek OSError [Errno 22] EINVAL test -mkfs OSError -mount OSError -ilistdir OSError -remove OSError -rmdir OSError -rename OSError -mkdir OSError -chdir OSError +mkfs OSError True +mount OSError True +ilistdir OSError [Errno 2] ENOENT +remove OSError [Errno 2] ENOENT +rmdir OSError [Errno 2] ENOENT +rename OSError [Errno 2] ENOENT +mkdir OSError [Errno 17] EEXIST +chdir OSError [Errno 2] ENOENT / -chdir OSError +chdir OSError [Errno 2] ENOENT / -stat OSError -seek OSError +stat OSError [Errno 2] ENOENT +seek OSError [Errno 22] EINVAL diff --git a/tests/extmod/vfs_lfs_ilistdir_del.py b/tests/extmod/vfs_lfs_ilistdir_del.py index 7b59bc412d9..828c85a2588 100644 --- a/tests/extmod/vfs_lfs_ilistdir_del.py +++ b/tests/extmod/vfs_lfs_ilistdir_del.py @@ -1,8 +1,7 @@ # Test ilistdir __del__ for VfsLittle using a RAM device. -import gc try: - import vfs + import gc, vfs vfs.VfsLfs2 except (ImportError, AttributeError): @@ -71,5 +70,10 @@ def test(bdev, vfs_class): fs.open("/test", "w").close() -bdev = RAMBlockDevice(30) +try: + bdev = RAMBlockDevice(30) +except MemoryError: + print("SKIP") + raise SystemExit + test(bdev, vfs.VfsLfs2) diff --git a/tests/extmod/vfs_mountinfo.py b/tests/extmod/vfs_mountinfo.py new file mode 100644 index 00000000000..b31dc60ce76 --- /dev/null +++ b/tests/extmod/vfs_mountinfo.py @@ -0,0 +1,65 @@ +# test VFS functionality without any particular filesystem type + +try: + import os, vfs +except ImportError: + print("SKIP") + raise SystemExit + + +class Filesystem: + def __init__(self, id, paths=[]): + self.id = id + self.paths = paths + + def mount(self, readonly, mksfs): + print("mount", self) + + def umount(self): + print("umount", self) + + def stat(self, path): + if path in self.paths: + return (0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + else: + raise OSError + + statvfs = stat + + def open(self, path, mode): + pass + + def __repr__(self): + return "Filesystem(%d)" % self.id + + +# first we umount any existing mount points the target may have +try: + vfs.umount("/") +except OSError: + pass +for path in os.listdir("/"): + vfs.umount("/" + path) + + +print(vfs.mount()) + +vfs.mount(Filesystem(1), "/foo") + +print(vfs.mount()) + +vfs.mount(Filesystem(2), "/bar/baz") + +print(vfs.mount()) + +vfs.mount(Filesystem(3), "/bar") + +print(vfs.mount()) + +vfs.umount("/bar/baz") + +print(vfs.mount()) + +vfs.mount(Filesystem(4), "/") + +print(vfs.mount()) diff --git a/tests/extmod/vfs_mountinfo.py.exp b/tests/extmod/vfs_mountinfo.py.exp new file mode 100644 index 00000000000..4ddf06c8c97 --- /dev/null +++ b/tests/extmod/vfs_mountinfo.py.exp @@ -0,0 +1,11 @@ +[] +mount Filesystem(1) +[(Filesystem(1), '/foo')] +mount Filesystem(2) +[(Filesystem(1), '/foo'), (Filesystem(2), '/bar/baz')] +mount Filesystem(3) +[(Filesystem(1), '/foo'), (Filesystem(2), '/bar/baz'), (Filesystem(3), '/bar')] +umount Filesystem(2) +[(Filesystem(1), '/foo'), (Filesystem(3), '/bar')] +mount Filesystem(4) +[(Filesystem(1), '/foo'), (Filesystem(3), '/bar'), (Filesystem(4), '/')] diff --git a/tests/extmod/vfs_posix.py b/tests/extmod/vfs_posix.py index d060c0b9c84..b3ca2753ba9 100644 --- a/tests/extmod/vfs_posix.py +++ b/tests/extmod/vfs_posix.py @@ -29,7 +29,21 @@ print(type(os.stat("/"))) # listdir and ilistdir -print(type(os.listdir("/"))) +target = "/" +try: + import platform + + # On Android non-root users are permitted full filesystem access only to + # selected directories. To let this test pass on bionic, the internal + # user-accessible storage area root is enumerated instead of the + # filesystem root. "/storage/emulated/0" should be there on pretty much + # any recent-ish device; querying the proper location requires a JNI + # round-trip, not really worth it. + if platform.platform().startswith("Android-"): + target = "/storage/emulated/0" +except ImportError: + pass +print(type(os.listdir(target))) # mkdir os.mkdir(temp_dir) diff --git a/tests/extmod/vfs_posix_ilistdir_del.py b/tests/extmod/vfs_posix_ilistdir_del.py index 78d7c854c54..8b5984cd81c 100644 --- a/tests/extmod/vfs_posix_ilistdir_del.py +++ b/tests/extmod/vfs_posix_ilistdir_del.py @@ -1,8 +1,7 @@ # Test ilistdir __del__ for VfsPosix. -import gc try: - import os, vfs + import gc, os, vfs vfs.VfsPosix except (ImportError, AttributeError): diff --git a/tests/extmod/vfs_posix_paths.py b/tests/extmod/vfs_posix_paths.py index b4fedc6716f..c06318748a3 100644 --- a/tests/extmod/vfs_posix_paths.py +++ b/tests/extmod/vfs_posix_paths.py @@ -31,7 +31,7 @@ fs.mkdir("subdir/one") print('listdir("/"):', sorted(i[0] for i in fs.ilistdir("/"))) print('listdir("."):', sorted(i[0] for i in fs.ilistdir("."))) -print('getcwd() in {"", "/"}:', fs.getcwd() in {"", "/"}) +print('getcwd() in ("", "/"):', fs.getcwd() in ("", "/")) print('chdir("subdir"):', fs.chdir("subdir")) print("getcwd():", fs.getcwd()) print('mkdir("two"):', fs.mkdir("two")) diff --git a/tests/extmod/vfs_posix_paths.py.exp b/tests/extmod/vfs_posix_paths.py.exp index ecc13222aaa..d6a0960efd2 100644 --- a/tests/extmod/vfs_posix_paths.py.exp +++ b/tests/extmod/vfs_posix_paths.py.exp @@ -1,6 +1,6 @@ listdir("/"): ['subdir'] listdir("."): ['subdir'] -getcwd() in {"", "/"}: True +getcwd() in ("", "/"): True chdir("subdir"): None getcwd(): /subdir mkdir("two"): None diff --git a/tests/extmod/vfs_posix_readonly.py b/tests/extmod/vfs_posix_readonly.py new file mode 100644 index 00000000000..e7821381006 --- /dev/null +++ b/tests/extmod/vfs_posix_readonly.py @@ -0,0 +1,99 @@ +# Test for VfsPosix + +try: + import gc, os, vfs, errno + + vfs.VfsPosix +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit + +# We need a directory for testing that doesn't already exist. +# Skip the test if it does exist. +temp_dir = "vfs_posix_readonly_test_dir" +try: + os.stat(temp_dir) + raise SystemExit("Target directory {} exists".format(temp_dir)) +except OSError: + pass + +# mkdir (skip test if whole filesystem is readonly) +try: + os.mkdir(temp_dir) +except OSError as e: + if e.errno == errno.EROFS: + print("SKIP") + raise SystemExit + +fs_factory = lambda: vfs.VfsPosix(temp_dir) + +# mount +fs = fs_factory() +vfs.mount(fs, "/vfs") + +with open("/vfs/file", "w") as f: + f.write("content") + +# test reading works +with open("/vfs/file") as f: + print("file:", f.read()) + +os.mkdir("/vfs/emptydir") + +# umount +vfs.umount("/vfs") + +# mount read-only +fs = fs_factory() +vfs.mount(fs, "/vfs", readonly=True) + +# test reading works +with open("/vfs/file") as f: + print("file 2:", f.read()) + +# test writing fails +try: + with open("/vfs/test_write", "w"): + pass + print("opened") +except OSError as er: + print(repr(er)) + +# test removing fails +try: + os.unlink("/vfs/file") + print("unlinked") +except OSError as er: + print(repr(er)) + +# test renaming fails +try: + os.rename("/vfs/file2", "/vfs/renamed") + print("renamed") +except OSError as er: + print(repr(er)) + +# test removing directory fails +try: + os.rmdir("/vfs/emptydir") + print("rmdir'd") +except OSError as er: + print(repr(er)) + +# test creating directory fails +try: + os.mkdir("/vfs/emptydir2") + print("mkdir'd") +except OSError as er: + print(repr(er)) + +# umount +vfs.umount("/vfs") + +fs = fs_factory() +vfs.mount(fs, "/vfs") + +os.rmdir("/vfs/emptydir") +os.unlink("/vfs/file") + +os.rmdir(temp_dir) diff --git a/tests/extmod/vfs_posix_readonly.py.exp b/tests/extmod/vfs_posix_readonly.py.exp new file mode 100644 index 00000000000..40e4316775f --- /dev/null +++ b/tests/extmod/vfs_posix_readonly.py.exp @@ -0,0 +1,7 @@ +file: content +file 2: content +OSError(30,) +OSError(30,) +OSError(30,) +OSError(30,) +OSError(30,) diff --git a/tests/extmod/vfs_rom.py b/tests/extmod/vfs_rom.py new file mode 100644 index 00000000000..18ae1f5cf96 --- /dev/null +++ b/tests/extmod/vfs_rom.py @@ -0,0 +1,489 @@ +# Test VfsRom filesystem. + +try: + import errno, sys, struct, os, uctypes, vfs + + vfs.VfsRom +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit + +try: + import select +except ImportError: + select = None + +import unittest + +IFDIR = 0x4000 +IFREG = 0x8000 + +SEEK_SET = 0 +SEEK_CUR = 1 +SEEK_END = 2 + +# An mpy file with four constant objects: str, bytes, long-int, float. +test_mpy = ( + # header + b"M\x06\x00\x1e" # mpy file header, -msmall-int-bits=30 + b"\x06" # n_qstr + b"\x05" # n_obj + # qstrs + b"\x0etest.py\x00" # qstr0 = "test.py" + b"\x0f" # qstr1 = "" + b"\x0estr_obj\x00" # qstr2 = "str_obj" + b"\x12bytes_obj\x00" # qstr3 = "bytes_obj" + b"\x0eint_obj\x00" # qstr4 = "int_obj" + b"\x12float_obj\x00" # qstr5 = "float_obj" + # objects + b"\x05\x14this is a str object\x00" + b"\x06\x16this is a bytes object\x00" + b"\x07\x0a1234567890" # long-int object + b"\x08\x041.23" # float object + b"\x05\x07str_obj\x00" # str object of existing qstr + # bytecode + b"\x81\x28" # 21 bytes, no children, bytecode + b"\x00\x02" # prelude + b"\x01" # simple name () + b"\x23\x00" # LOAD_CONST_OBJ(0) + b"\x16\x02" # STORE_NAME(str_obj) + b"\x23\x01" # LOAD_CONST_OBJ(1) + b"\x16\x03" # STORE_NAME(bytes_obj) + b"\x23\x02" # LOAD_CONST_OBJ(2) + b"\x16\x04" # STORE_NAME(int_obj) + b"\x23\x03" # LOAD_CONST_OBJ(3) + b"\x16\x05" # STORE_NAME(float_obj) + b"\x51" # LOAD_CONST_NONE + b"\x63" # RETURN_VALUE +) + + +class VfsRomWriter: + ROMFS_HEADER = b"\xd2\xcd\x31" + + ROMFS_RECORD_KIND_UNUSED = 0 + ROMFS_RECORD_KIND_PADDING = 1 + ROMFS_RECORD_KIND_DATA_VERBATIM = 2 + ROMFS_RECORD_KIND_DATA_POINTER = 3 + ROMFS_RECORD_KIND_DIRECTORY = 4 + ROMFS_RECORD_KIND_FILE = 5 + + def __init__(self): + self._dir_stack = [(None, bytearray())] + + def _encode_uint(self, value): + encoded = [value & 0x7F] + value >>= 7 + while value != 0: + encoded.insert(0, 0x80 | (value & 0x7F)) + value >>= 7 + return bytes(encoded) + + def _pack(self, kind, payload): + return self._encode_uint(kind) + self._encode_uint(len(payload)) + payload + + def _extend(self, data): + buf = self._dir_stack[-1][1] + buf.extend(data) + return len(buf) + + def finalise(self): + _, data = self._dir_stack.pop() + encoded_kind = VfsRomWriter.ROMFS_HEADER + encoded_len = self._encode_uint(len(data)) + if (len(encoded_kind) + len(encoded_len) + len(data)) % 2 == 1: + encoded_len = b"\x80" + encoded_len + data = encoded_kind + encoded_len + data + return data + + def opendir(self, dirname): + self._dir_stack.append((dirname, bytearray())) + + def closedir(self): + dirname, dirdata = self._dir_stack.pop() + dirdata = self._encode_uint(len(dirname)) + bytes(dirname, "ascii") + dirdata + self._extend(self._pack(VfsRomWriter.ROMFS_RECORD_KIND_DIRECTORY, dirdata)) + + def mkdata(self, data): + assert len(self._dir_stack) == 1 + return self._extend(self._pack(VfsRomWriter.ROMFS_RECORD_KIND_DATA_VERBATIM, data)) - len( + data + ) + + def mkfile(self, filename, filedata, extra_payload=b""): + filename = bytes(filename, "ascii") + payload = self._encode_uint(len(filename)) + payload += filename + payload += extra_payload + if isinstance(filedata, tuple): + sub_payload = self._encode_uint(filedata[0]) + sub_payload += self._encode_uint(filedata[1]) + payload += self._pack(VfsRomWriter.ROMFS_RECORD_KIND_DATA_POINTER, sub_payload) + else: + payload += self._pack(VfsRomWriter.ROMFS_RECORD_KIND_DATA_VERBATIM, filedata) + self._dir_stack[-1][1].extend(self._pack(VfsRomWriter.ROMFS_RECORD_KIND_FILE, payload)) + + +def _make_romfs(fs, files, data_map): + for filename, contents in files: + if isinstance(contents, tuple): + fs.opendir(filename) + _make_romfs(fs, contents, data_map) + fs.closedir() + elif isinstance(contents, int): + fs.mkfile(filename, data_map[contents]) + else: + fs.mkfile(filename, contents) + + +def make_romfs(files, data=None): + fs = VfsRomWriter() + data_map = {} + if data: + for k, v in data.items(): + data_map[k] = len(v), fs.mkdata(v) + _make_romfs(fs, files, data_map) + return fs.finalise() + + +# A class to test if a value is within a range, needed because MicroPython's range +# doesn't support arbitrary objects. +class Range: + def __init__(self, lower, upper): + self._lower = lower + self._upper = upper + + def __repr__(self): + return "Range({}, {})".format(self._lower, self._upper) + + def __contains__(self, value): + return self._lower <= value < self._upper + + +class TestBase(unittest.TestCase): + @classmethod + def setUpClass(cls): + fs_inner = make_romfs((("test_inner.txt", b"contents_inner"), ("c.py", b""))) + cls.romfs = make_romfs( + ( + ("fs.romfs", 0), + ("test.txt", b"contents"), + ( + "dir", + ( + ("a.py", b"x = 1"), + ("b.py", b"x = 2"), + ("test.mpy", test_mpy), + ), + ), + ), + {0: fs_inner}, + ) + cls.romfs_ilistdir = [ + ("fs.romfs", IFREG, 0, 46), + ("test.txt", IFREG, 0, 8), + ("dir", IFDIR, 0, 198), + ] + cls.romfs_listdir = [x[0] for x in cls.romfs_ilistdir] + cls.romfs_listdir_dir = ["a.py", "b.py", "test.mpy"] + cls.romfs_listdir_bytes = [bytes(x, "ascii") for x in cls.romfs_listdir] + cls.romfs_addr = uctypes.addressof(cls.romfs) + cls.romfs_addr_range = Range(cls.romfs_addr, cls.romfs_addr + len(cls.romfs)) + + +class TestEdgeCases(unittest.TestCase): + def test_empty_romfs(self): + empty_romfs = make_romfs(()) + self.assertEqual(empty_romfs, bytes([0x80 | ord("R"), 0x80 | ord("M"), ord("1"), 0])) + fs = vfs.VfsRom(empty_romfs) + self.assertIsInstance(fs, vfs.VfsRom) + fs.mount(True, False) + self.assertEqual(list(fs.ilistdir("")), []) + self.assertEqual(fs.stat(""), (IFDIR, 0, 0, 0, 0, 0, 0, 0, 0, 0)) + self.assertEqual(fs.statvfs(""), (1, 0, 0, 0, 0, 0, 0, 0, 0, 32767)) + + def test_error(self): + for bad_romfs in (b"", b"xxx", b"not a romfs"): + with self.assertRaises(OSError) as ctx: + vfs.VfsRom(bad_romfs) + self.assertEqual(ctx.exception.errno, errno.ENODEV) + + def test_unknown_record(self): + fs = VfsRomWriter() + fs._extend(fs._pack(VfsRomWriter.ROMFS_RECORD_KIND_PADDING, b"payload")) + fs.mkfile( + "test", + b"contents", + extra_payload=fs._pack(VfsRomWriter.ROMFS_RECORD_KIND_PADDING, b"pad"), + ) + romfs = fs.finalise() + fs = vfs.VfsRom(romfs) + self.assertEqual(list(fs.ilistdir("")), [("test", IFREG, 0, 8)]) + with fs.open("test", "rb") as f: + self.assertEqual(f.read(), b"contents") + + +class TestCorrupt(unittest.TestCase): + def test_corrupt_filesystem(self): + # Make the filesystem length bigger than the buffer. + romfs = bytearray(make_romfs(())) + romfs[3] = 0x01 + with self.assertRaises(OSError): + vfs.VfsRom(romfs) + + # Corrupt the filesystem length. + romfs = bytearray(make_romfs(())) + romfs[3] = 0xFF + with self.assertRaises(OSError): + vfs.VfsRom(romfs) + + # Corrupt the contents of the filesystem. + romfs = bytearray(make_romfs(())) + romfs[3] = 0x01 + romfs.extend(b"\xff\xff") + fs = vfs.VfsRom(romfs) + with self.assertRaises(OSError): + fs.stat("a") + self.assertEqual(list(fs.ilistdir("")), []) + + def test_corrupt_file_entry(self): + romfs = make_romfs((("file", b"data"),)) + + # Corrupt the length of filename. + romfs_corrupt = bytearray(romfs) + romfs_corrupt[7:] = b"\xff" * (len(romfs) - 7) + fs = vfs.VfsRom(romfs_corrupt) + with self.assertRaises(OSError): + fs.stat("file") + self.assertEqual(list(fs.ilistdir("")), []) + + # Erase the data record (change it to a padding record). + romfs_corrupt = bytearray(romfs) + romfs_corrupt[12] = VfsRomWriter.ROMFS_RECORD_KIND_PADDING + fs = vfs.VfsRom(romfs_corrupt) + with self.assertRaises(OSError): + fs.stat("file") + self.assertEqual(list(fs.ilistdir("")), []) + + # Corrupt the header of the data record. + romfs_corrupt = bytearray(romfs) + romfs_corrupt[12:] = b"\xff" * (len(romfs) - 12) + fs = vfs.VfsRom(romfs_corrupt) + with self.assertRaises(OSError): + fs.stat("file") + + # Corrupt the interior of the data record. + romfs_corrupt = bytearray(romfs) + romfs_corrupt[13:] = b"\xff" * (len(romfs) - 13) + fs = vfs.VfsRom(romfs_corrupt) + with self.assertRaises(OSError): + fs.stat("file") + + # Change the data record to an indirect pointer and corrupt the length. + romfs_corrupt = bytearray(romfs) + romfs_corrupt[12] = VfsRomWriter.ROMFS_RECORD_KIND_DATA_POINTER + romfs_corrupt[14:18] = b"\xff\xff\xff\xff" + fs = vfs.VfsRom(romfs_corrupt) + with self.assertRaises(OSError): + fs.stat("file") + + # Change the data record to an indirect pointer and corrupt the offset. + romfs_corrupt = bytearray(romfs) + romfs_corrupt[12] = VfsRomWriter.ROMFS_RECORD_KIND_DATA_POINTER + romfs_corrupt[14:18] = b"\x00\xff\xff\xff" + fs = vfs.VfsRom(romfs_corrupt) + with self.assertRaises(OSError): + fs.stat("file") + + +class TestStandalone(TestBase): + def test_constructor(self): + self.assertIsInstance(vfs.VfsRom(self.romfs), vfs.VfsRom) + with self.assertRaises(TypeError): + vfs.VfsRom(self.romfs_addr) + + def test_mount(self): + vfs.VfsRom(self.romfs).mount(True, False) + with self.assertRaises(OSError): + vfs.VfsRom(self.romfs).mount(True, True) + + def test_ilistdir(self): + fs = vfs.VfsRom(self.romfs) + self.assertEqual(list(fs.ilistdir("")), self.romfs_ilistdir) + self.assertEqual(list(fs.ilistdir("/")), self.romfs_ilistdir) + with self.assertRaises(OSError): + fs.ilistdir("does not exist") + + def test_stat(self): + fs = vfs.VfsRom(self.romfs) + self.assertEqual(fs.stat(""), (IFDIR, 0, 0, 0, 0, 0, 289, 0, 0, 0)) + self.assertEqual(fs.stat("/"), (IFDIR, 0, 0, 0, 0, 0, 289, 0, 0, 0)) + self.assertEqual(fs.stat("/test.txt"), (IFREG, 0, 0, 0, 0, 0, 8, 0, 0, 0)) + self.assertEqual(fs.stat("/dir"), (IFDIR, 0, 0, 0, 0, 0, 198, 0, 0, 0)) + with self.assertRaises(OSError): + fs.stat("/does-not-exist") + + def test_statvfs(self): + fs = vfs.VfsRom(self.romfs) + self.assertEqual(fs.statvfs(""), (1, 0, 289, 0, 0, 0, 0, 0, 0, 32767)) + + def test_open(self): + fs = vfs.VfsRom(self.romfs) + + with fs.open("/test.txt", "") as f: + self.assertEqual(f.read(), "contents") + with fs.open("/test.txt", "rt") as f: + self.assertEqual(f.read(), "contents") + with fs.open("/test.txt", "rb") as f: + self.assertEqual(f.read(), b"contents") + + with self.assertRaises(OSError) as ctx: + fs.open("/file-does-not-exist", "") + self.assertEqual(ctx.exception.errno, errno.ENOENT) + + with self.assertRaises(OSError) as ctx: + fs.open("/dir", "rb") + self.assertEqual(ctx.exception.errno, errno.EISDIR) + + with self.assertRaises(OSError): + fs.open("/test.txt", "w") + with self.assertRaises(OSError): + fs.open("/test.txt", "a") + with self.assertRaises(OSError): + fs.open("/test.txt", "+") + + def test_file_seek(self): + fs = vfs.VfsRom(self.romfs) + with fs.open("/test.txt", "") as f: + self.assertEqual(f.seek(0, SEEK_SET), 0) + self.assertEqual(f.seek(3, SEEK_SET), 3) + self.assertEqual(f.read(), "tents") + self.assertEqual(f.seek(0, SEEK_SET), 0) + self.assertEqual(f.seek(100, SEEK_CUR), 8) + self.assertEqual(f.seek(-1, SEEK_END), 7) + self.assertEqual(f.read(), "s") + self.assertEqual(f.seek(1, SEEK_END), 8) + with self.assertRaises(OSError): + f.seek(-1, SEEK_SET) + f.seek(0, SEEK_SET) + with self.assertRaises(OSError): + f.seek(-1, SEEK_CUR) + with self.assertRaises(OSError): + f.seek(-100, SEEK_END) + + @unittest.skipIf(select is None, "no select module") + def test_file_ioctl_invalid(self): + fs = vfs.VfsRom(self.romfs) + with fs.open("/test.txt", "") as f: + p = select.poll() + p.register(f) + with self.assertRaises(OSError): + p.poll(0) + + def test_memory_mapping(self): + fs = vfs.VfsRom(self.romfs) + with fs.open("/test.txt", "rb") as f: + addr = uctypes.addressof(f) + data = memoryview(f) + self.assertIn(addr, self.romfs_addr_range) + self.assertIn(addr + len(data), self.romfs_addr_range) + self.assertEqual(bytes(data), b"contents") + + +class TestMounted(TestBase): + def setUp(self): + self.orig_sys_path = list(sys.path) + self.orig_cwd = os.getcwd() + sys.path = [] + vfs.mount(vfs.VfsRom(self.romfs), "/test_rom") + + def tearDown(self): + vfs.umount("/test_rom") + os.chdir(self.orig_cwd) + sys.path = self.orig_sys_path + + def test_listdir(self): + self.assertEqual(os.listdir("/test_rom"), self.romfs_listdir) + self.assertEqual(os.listdir("/test_rom/dir"), self.romfs_listdir_dir) + self.assertEqual(os.listdir(b"/test_rom"), self.romfs_listdir_bytes) + + def test_chdir(self): + os.chdir("/test_rom") + self.assertEqual(os.getcwd(), "/test_rom") + self.assertEqual(os.listdir(), self.romfs_listdir) + + os.chdir("/test_rom/") + self.assertEqual(os.getcwd(), "/test_rom") + self.assertEqual(os.listdir(), self.romfs_listdir) + + # chdir within the romfs is not implemented. + with self.assertRaises(OSError): + os.chdir("/test_rom/dir") + + def test_stat(self): + self.assertEqual(os.stat("/test_rom"), (IFDIR, 0, 0, 0, 0, 0, 289, 0, 0, 0)) + self.assertEqual(os.stat("/test_rom/"), (IFDIR, 0, 0, 0, 0, 0, 289, 0, 0, 0)) + self.assertEqual(os.stat("/test_rom/test.txt"), (IFREG, 0, 0, 0, 0, 0, 8, 0, 0, 0)) + self.assertEqual(os.stat("/test_rom/dir"), (IFDIR, 0, 0, 0, 0, 0, 198, 0, 0, 0)) + with self.assertRaises(OSError): + os.stat("/test_rom/does-not-exist") + + def test_open(self): + with open("/test_rom/test.txt") as f: + self.assertEqual(f.read(), "contents") + with open("/test_rom/test.txt", "b") as f: + self.assertEqual(f.read(), b"contents") + + with self.assertRaises(OSError) as ctx: + open("/test_rom/file-does-not-exist") + self.assertEqual(ctx.exception.errno, errno.ENOENT) + + with self.assertRaises(OSError) as ctx: + open("/test_rom/dir") + self.assertEqual(ctx.exception.errno, errno.EISDIR) + + def test_import_py(self): + sys.path.append("/test_rom/dir") + a = __import__("a") + b = __import__("b") + self.assertEqual(a.__file__, "/test_rom/dir/a.py") + self.assertEqual(a.x, 1) + self.assertEqual(b.__file__, "/test_rom/dir/b.py") + self.assertEqual(b.x, 2) + + def test_import_mpy(self): + sys.path.append("/test_rom/dir") + test = __import__("test") + self.assertEqual(test.__file__, "/test_rom/dir/test.mpy") + self.assertEqual(test.str_obj, "this is a str object") + self.assertEqual(test.bytes_obj, b"this is a bytes object") + self.assertEqual(test.int_obj, 1234567890) + self.assertEqual(test.float_obj, 1.23) + self.assertIn(uctypes.addressof(test.str_obj), self.romfs_addr_range) + self.assertIn(uctypes.addressof(test.bytes_obj), self.romfs_addr_range) + + def test_romfs_inner(self): + with open("/test_rom/fs.romfs", "rb") as f: + romfs_inner = vfs.VfsRom(memoryview(f)) + + vfs.mount(romfs_inner, "/test_rom2") + + self.assertEqual(os.listdir("/test_rom2"), ["test_inner.txt", "c.py"]) + + sys.path.append("/test_rom2") + self.assertEqual(__import__("c").__file__, "/test_rom2/c.py") + + with open("/test_rom2/test_inner.txt") as f: + self.assertEqual(f.read(), "contents_inner") + + with open("/test_rom2/test_inner.txt", "rb") as f: + addr = uctypes.addressof(f) + data = memoryview(f) + self.assertIn(addr, self.romfs_addr_range) + self.assertIn(addr + len(data), self.romfs_addr_range) + + vfs.umount("/test_rom2") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/extmod/websocket_basic.py b/tests/extmod/websocket_basic.py index 133457784f6..6ebd65d655f 100644 --- a/tests/extmod/websocket_basic.py +++ b/tests/extmod/websocket_basic.py @@ -13,6 +13,13 @@ def ws_read(msg, sz): return ws.read(sz) +# put raw data in the stream and do a series of websocket read +def ws_readn(msg, *args): + ws = websocket.websocket(io.BytesIO(msg)) + for sz in args: + yield ws.read(sz) + + # do a websocket write and then return the raw data from the stream def ws_write(msg, sz): s = io.BytesIO() @@ -24,18 +31,28 @@ def ws_write(msg, sz): # basic frame print(ws_read(b"\x81\x04ping", 4)) -print(ws_read(b"\x80\x04ping", 4)) # FRAME_CONT print(ws_write(b"pong", 6)) -# split frames are not supported -# print(ws_read(b"\x01\x04ping", 4)) +# split frames and irregular size reads +for s in ws_readn(b"\x01\x04ping\x00\x04Ping\x80\x04PING", 6, 4, 2, 2): + print(s) # extended payloads print(ws_read(b"\x81~\x00\x80" + b"ping" * 32, 128)) print(ws_write(b"pong" * 32, 132)) -# mask (returned data will be 'mask' ^ 'mask') -print(ws_read(b"\x81\x84maskmask", 4)) +# 64-bit payload size, unsupported by MicroPython implementation. Framing is lost. +msg = b"\x81\x7f\x00\x00\x00\x00\x00\x00\x00\x80" + b"ping" * 32 +ws = websocket.websocket(io.BytesIO(msg)) +try: + print(ws.read(1)) +except OSError as e: + print("ioctl: EIO:", e.errno == errno.EIO) + +# mask (returned data will be 'maskmask' ^ 'maskMASK') +print(ws_read(b"\x81\x88maskmaskMASK", 8)) +# mask w/2-byte payload len (returned data will be 'maskmask' ^ 'maskMASK') +print(ws_read(b"\x81\xfe\x00\x08maskmaskMASK", 8)) # close control frame s = io.BytesIO(b"\x88\x00") # FRAME_CLOSE diff --git a/tests/extmod/websocket_basic.py.exp b/tests/extmod/websocket_basic.py.exp index 152aae836d6..e4164ac69b1 100644 --- a/tests/extmod/websocket_basic.py.exp +++ b/tests/extmod/websocket_basic.py.exp @@ -1,9 +1,14 @@ b'ping' -b'ping' b'\x81\x04pong' +b'pingPi' +b'ngPI' +b'NG' +b'' b'pingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingpingping' b'\x81~\x00\x80pongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpongpong' -b'\x00\x00\x00\x00' +ioctl: EIO: True +b'\x00\x00\x00\x00 ' +b'\x00\x00\x00\x00 ' b'' b'\x88\x00' b'ping' diff --git a/tests/extmod/websocket_toobig.py b/tests/extmod/websocket_toobig.py new file mode 100644 index 00000000000..f4c5a74bbce --- /dev/null +++ b/tests/extmod/websocket_toobig.py @@ -0,0 +1,28 @@ +try: + import io + import errno + import websocket +except ImportError: + print("SKIP") + raise SystemExit + +try: + buf = "x" * 65536 +except MemoryError: + print("SKIP") + raise SystemExit + + +# do a websocket write and then return the raw data from the stream +def ws_write(msg, sz): + s = io.BytesIO() + ws = websocket.websocket(s) + ws.write(msg) + s.seek(0) + return s.read(sz) + + +try: + print(ws_write(buf, 1)) +except OSError as e: + print("ioctl: ENOBUFS:", e.errno == errno.ENOBUFS) diff --git a/tests/extmod/websocket_toobig.py.exp b/tests/extmod/websocket_toobig.py.exp new file mode 100644 index 00000000000..3bbd95282fd --- /dev/null +++ b/tests/extmod/websocket_toobig.py.exp @@ -0,0 +1 @@ +ioctl: ENOBUFS: True diff --git a/tests/extmod_hardware/machine_can2.py b/tests/extmod_hardware/machine_can2.py new file mode 100644 index 00000000000..0ecced82865 --- /dev/null +++ b/tests/extmod_hardware/machine_can2.py @@ -0,0 +1,44 @@ +# Test machine.CAN(1) and machine.CAN(2) using loopback +# +# Single device test, assumes support for loopback and no connections to the CAN pins +# +# This test is ported from tests/ports/stm32/pyb_can2.py + +try: + from machine import CAN + + CAN(2, 125_000) +except (ImportError, ValueError): + print("SKIP") + raise SystemExit + +import time + +# Setting up each CAN peripheral independently is deliberate here, to catch +# catch cases where initialising CAN2 breaks CAN1 + +can1 = CAN(1, 125_000, mode=CAN.MODE_LOOPBACK) +can1.set_filters([(0x100, 0x700, 0)]) + +can2 = CAN(2, 125_000, mode=CAN.MODE_LOOPBACK) +can2.set_filters([(0x000, 0x7F0, 0)]) + +# Drain any old messages in RX FIFOs +for can in (can1, can2): + while can.recv(): + pass + +for id, can in ((1, can1), (2, can2)): + print("testing", id) + # message1 should only receive on can1, message2 on can2 + can.send(0x123, b"message1", 0) + can.send(0x003, "message2", 0) + time.sleep_ms(10) + did_recv = False + while res := can.recv(): + did_recv = True + print(hex(res[0]), bytes(res[1]), res[2], res[3]) + if not did_recv: + print("no rx!") + +print("done") diff --git a/tests/extmod_hardware/machine_can2.py.exp b/tests/extmod_hardware/machine_can2.py.exp new file mode 100644 index 00000000000..bfb6a5088ba --- /dev/null +++ b/tests/extmod_hardware/machine_can2.py.exp @@ -0,0 +1,5 @@ +testing 1 +0x123 b'message1' 0 0 +testing 2 +0x3 b'message2' 0 0 +done diff --git a/tests/extmod_hardware/machine_can_timings.py b/tests/extmod_hardware/machine_can_timings.py new file mode 100644 index 00000000000..441059f5da5 --- /dev/null +++ b/tests/extmod_hardware/machine_can_timings.py @@ -0,0 +1,60 @@ +# Test machine.CAN timings results +# +# Single device test, assumes no connections to the CAN pins + +try: + from machine import CAN +except ImportError: + print("SKIP") + raise SystemExit + +import unittest + +from target_wiring import can_args, can_kwargs + + +class TestTimings(unittest.TestCase): + def test_bitrate(self): + for bitrate in (125_000, 250_000, 500_000, 1_000_000): + can = CAN(*can_args, bitrate=bitrate, **can_kwargs) + print(can) + timings = can.get_timings() + print(timings) + # Actual bitrate may not be exactly equal to requested rate + self.assertAlmostEqual(timings[0], bitrate, delta=1_000) + can.deinit() + + def test_sample_point(self): + # Verify that tseg1 and tseg2 are set correctly from the sample_point argument + for sample_point in (66, 75, 95): + can = CAN(*can_args, bitrate=500_000, sample_point=sample_point, **can_kwargs) + _bitrate, _sjw, tseg1, tseg2, _fd, _port = can.get_timings() + print(f"sample_point={sample_point}, tseg1={tseg1}, tseg2={tseg2}") + self.assertAlmostEqual(sample_point / 100, tseg1 / (tseg1 + tseg2), delta=0.05) + can.deinit() + + def test_tseg_args(self): + # Verify that tseg1 and tseg2 are set correctly and sample_point is ignored if these are provided + for tseg1, tseg2 in ((5, 2), (16, 8), (16, 5), (15, 5)): + print(f"tseg1={tseg1} tseg2={tseg2}") + can = CAN( + *can_args, bitrate=250_000, tseg1=tseg1, tseg2=tseg2, sample_point=99, **can_kwargs + ) + bitrate, _sjw, ret_tseg1, ret_tseg2, _fd, _port = can.get_timings() + self.assertEqual(ret_tseg1, tseg1) + self.assertEqual(ret_tseg2, tseg2) + + def test_invalid_timing_args(self): + # Test various kwargs out of their allowed value ranges + with self.assertRaises(ValueError): + CAN(*can_args, bitrate=250_000, tseg1=55, **can_kwargs) + with self.assertRaises(ValueError): + CAN(*can_args, bitrate=500_000, tseg2=9, **can_kwargs) + with self.assertRaises(ValueError): + CAN(*can_args, bitrate=-1, **can_kwargs) + with self.assertRaises(ValueError): + CAN(*can_args, bitrate=500_000, sample_point=101, **can_kwargs) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/extmod_hardware/machine_counter.py b/tests/extmod_hardware/machine_counter.py new file mode 100644 index 00000000000..a91d262854e --- /dev/null +++ b/tests/extmod_hardware/machine_counter.py @@ -0,0 +1,101 @@ +# Test machine.Counter implementation +# +# IMPORTANT: This test requires hardware connections: the out_pin and in_pin +# must be wired together. + +try: + from machine import Counter +except ImportError: + print("SKIP") + raise SystemExit + +import sys +from machine import Pin + +if "esp32" in sys.platform: + id = 0 + out_pin = 4 + in_pin = 5 +elif sys.platform == "mimxrt": + if "Teensy" in sys.implementation._machine: + id = 0 + out_pin = "D2" + in_pin = "D3" +else: + print("Please add support for this test on this platform.") + raise SystemExit + +import unittest + +out_pin = Pin(out_pin, mode=Pin.OUT) +in_pin = Pin(in_pin, mode=Pin.IN) + + +def toggle(times): + for _ in range(times): + out_pin(1) + out_pin(0) + + +class TestConnections(unittest.TestCase): + def setUp(self): + in_pin.init(Pin.IN) + + def test_connections(self): + # Test the hardware connections are correct. If this test fails, all tests will fail. + out_pin(1) + self.assertEqual(1, in_pin()) + out_pin(0) + self.assertEqual(0, in_pin()) + + +class TestCounter(unittest.TestCase): + def setUp(self): + out_pin(0) + self.counter = Counter(id, in_pin) + + def tearDown(self): + self.counter.deinit() + + def assertCounter(self, value): + self.assertEqual(self.counter.value(), value) + + def test_count_rising(self): + self.assertCounter(0) + toggle(100) + self.assertCounter(100) + out_pin(1) + self.assertEqual(self.counter.value(0), 101) + self.assertCounter(0) # calling value(0) resets + out_pin(0) + self.assertCounter(0) # no rising edge + out_pin(1) + self.assertCounter(1) + + def test_change_directions(self): + self.assertCounter(0) + toggle(100) + self.assertCounter(100) + self.counter.init(in_pin, direction=Counter.DOWN) + self.assertCounter(0) # calling init() zeroes the counter + self.counter.value(100) # need to manually reset the value + self.assertCounter(100) + toggle(25) + self.assertCounter(75) + + @unittest.skipIf(sys.platform == "mimxrt", "FALLING edge not supported") + def test_count_falling(self): + self.counter.init(in_pin, direction=Counter.UP, edge=Counter.FALLING) + toggle(20) + self.assertCounter(20) + out_pin(1) + self.assertCounter(20) # no falling edge + out_pin(0) + self.assertCounter(21) + self.counter.value(-(2**24)) + toggle(20) + self.assertCounter(-(2**24 - 20)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/extmod_hardware/machine_encoder.py b/tests/extmod_hardware/machine_encoder.py new file mode 100644 index 00000000000..67d1b5f443e --- /dev/null +++ b/tests/extmod_hardware/machine_encoder.py @@ -0,0 +1,158 @@ +# Test machine.Encoder implementation +# +# IMPORTANT: This test requires hardware connections: +# - out0_pin and in0_pin must be wired together. +# - out1_pin and in1_pin must be wired together. + +try: + from machine import Encoder +except ImportError: + print("SKIP") + raise SystemExit + +import sys +import unittest +from machine import Pin +from target_wiring import encoder_loopback_id, encoder_loopback_out_pins, encoder_loopback_in_pins + +PRINT = False +PIN_INIT_VALUE = 1 + +id = encoder_loopback_id +out0_pin, out1_pin = encoder_loopback_out_pins +in0_pin, in1_pin = encoder_loopback_in_pins + +out0_pin = Pin(out0_pin, mode=Pin.OUT) +in0_pin = Pin(in0_pin, mode=Pin.IN) +out1_pin = Pin(out1_pin, mode=Pin.OUT) +in1_pin = Pin(in1_pin, mode=Pin.IN) + + +class TestConnections(unittest.TestCase): + def setUp(self): + in0_pin.init(Pin.IN) + in1_pin.init(Pin.IN) + + def test_connections(self): + # Test the hardware connections are correct. If this test fails, all tests will fail. + for ch, outp, inp in ((0, out0_pin, in0_pin), (1, out1_pin, in1_pin)): + print("Testing channel ", ch) + outp(1) + self.assertEqual(1, inp()) + outp(0) + self.assertEqual(0, inp()) + + +class TestEncoder(unittest.TestCase): + def setUp(self): + out0_pin(PIN_INIT_VALUE) + out1_pin(PIN_INIT_VALUE) + self.enc = Encoder(id, in0_pin, in1_pin, phases=1) + self.enc2 = Encoder(id + 1, in0_pin, in1_pin, phases=2) + self.enc4 = Encoder(id + 2, in0_pin, in1_pin, phases=4) + self.pulses = 0 # track the expected encoder position in software + if PRINT: + print( + "\nout0_pin() out1_pin() enc.value() enc2.value() enc4.value() |", + out0_pin(), + out1_pin(), + "|", + self.enc.value(), + self.enc2.value(), + self.enc4.value(), + ) + + def tearDown(self): + self.enc.deinit() + try: + self.enc2.deinit() + except: + pass + try: + self.enc4.deinit() + except: + pass + + def rotate(self, pulses): + for _ in range(abs(pulses)): + self.pulses += 1 if (pulses > 0) else -1 + if pulses > 0: + if self.pulses % 2: + out0_pin(not out0_pin()) + else: + out1_pin(not out1_pin()) + else: + if self.pulses % 2: + out1_pin(not out1_pin()) + else: + out0_pin(not out0_pin()) + if PRINT: + print( + "out0_pin() out1_pin() enc.value() enc2.value() enc4.value() pulses self.pulses |", + out0_pin(), + out1_pin(), + "|", + self.enc.value(), + self.enc2.value(), + self.enc4.value(), + "|", + pulses, + self.pulses, + ) + + def assertPosition(self, value, value2=None, value4=None): + self.assertEqual(self.enc.value(), value) + if not value2 is None: + self.assertEqual(self.enc2.value(), value2) + if not value4 is None: + self.assertEqual(self.enc4.value(), value4) + pass + + def test_basics(self): + self.assertPosition(0) + self.rotate(100) + self.assertPosition(100 // 4, 100 // 2, 100) + self.rotate(-100) + self.assertPosition(0) + + def test_partial(self): + # With phase=1 (default), need 4x pulses to count a rotation + self.assertPosition(0) + self.rotate(1) + self.assertPosition(1, 1, 1) + self.rotate(1) + self.assertPosition(1, 1, 2) + self.rotate(1) + self.assertPosition(1, 2, 3) + self.rotate(1) + self.assertPosition(1, 2, 4) # +4 + self.rotate(1) + self.assertPosition(2, 3, 5) + self.rotate(1) + self.assertPosition(2, 3, 6) + self.rotate(1) + self.assertPosition(2, 4, 7) + self.rotate(1) + self.assertPosition(2, 4, 8) # +4 + self.rotate(-1) + self.assertPosition(2, 4, 7) + self.rotate(-3) + self.assertPosition(1, 2, 4) # -4 + self.rotate(-4) + self.assertPosition(0, 0, 0) # -4 + self.rotate(-1) + self.assertPosition(0, 0, -1) + self.rotate(-1) + self.assertPosition(0, -1, -2) + self.rotate(-1) + self.assertPosition(0, -1, -3) + self.rotate(-1) + self.assertPosition(-1, -2, -4) # -4 + self.rotate(-1) + self.assertPosition(-1, -2, -5) + self.rotate(-3) + self.assertPosition(-2, -4, -8) # -4 + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/extmod_hardware/machine_i2c_target.py b/tests/extmod_hardware/machine_i2c_target.py new file mode 100644 index 00000000000..763e6f4771e --- /dev/null +++ b/tests/extmod_hardware/machine_i2c_target.py @@ -0,0 +1,307 @@ +# Test machine.I2CTarget. +# +# IMPORTANT: This test requires hardware connections: a SoftI2C instance must be +# wired to a hardware I2C target. See pin definitions below. + +import sys + +try: + from machine import Pin, SoftI2C, I2CTarget +except ImportError: + print("SKIP") + raise SystemExit + +import unittest + +ADDR = 67 + +kwargs_target = {} + +# Configure pins based on the target. +if sys.platform == "alif" and sys.implementation._build == "ALIF_ENSEMBLE": + args_controller = {"scl": "P1_1", "sda": "P1_0"} + args_target = (0,) # on pins P0_3/P0_2 +elif sys.platform == "esp32": + args_controller = {"scl": 5, "sda": 6} + args_target = (0,) # on pins 9/8 for C3 and S3, 18/19 for others + kwargs_target = {"scl": 9, "sda": 8} +elif sys.platform == "rp2": + args_controller = {"scl": 5, "sda": 4} + args_target = (1,) +elif sys.platform == "pyboard": + if sys.implementation._build == "NUCLEO_WB55": + args_controller = {"scl": "B8", "sda": "B9"} + args_target = (3,) + else: + args_controller = {"scl": "X1", "sda": "X2"} + args_target = ("X",) +elif "zephyr-nucleo_wb55rg" in sys.implementation._machine: + # PB8=I2C1_SCL, PB9=I2C1_SDA (on Arduino header D15/D14) + # PC0=I2C3_SCL, PC1=I2C3_SDA (on Arduino header A0/A1) + args_controller = {"scl": Pin(("gpiob", 8)), "sda": Pin(("gpiob", 9))} + args_target = ("i2c3",) +elif "zephyr-rpi_pico" in sys.implementation._machine: + args_controller = {"scl": Pin(("gpio0", 5)), "sda": Pin(("gpio0", 4))} + args_target = ("i2c1",) # on gpio7/gpio6 +elif sys.platform == "mimxrt": + if "Teensy" in sys.implementation._machine: + args_controller = {"scl": "A6", "sda": "A3"} # D20/D17 + else: + args_controller = {"scl": "D0", "sda": "D1"} + args_target = (0,) # pins 19/18 On Teensy 4.x +elif sys.platform == "samd": + args_controller = {"scl": "D5", "sda": "D1"} + args_target = () +else: + print("Please add support for this test on this platform.") + raise SystemExit + + +def config_pull_up(): + Pin(args_controller["scl"], Pin.OPEN_DRAIN, Pin.PULL_UP) + Pin(args_controller["sda"], Pin.OPEN_DRAIN, Pin.PULL_UP) + + +class TestMemory(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.mem = bytearray(8) + cls.i2c = SoftI2C(**args_controller) + cls.i2c_target = I2CTarget(*args_target, **kwargs_target, addr=ADDR, mem=cls.mem) + config_pull_up() + + @classmethod + def tearDownClass(cls): + cls.i2c_target.deinit() + + def test_scan(self): + self.assertIn(ADDR, self.i2c.scan()) + + def test_write(self): + self.mem[:] = b"01234567" + self.i2c.writeto_mem(ADDR, 0, b"test") + self.assertEqual(self.mem, bytearray(b"test4567")) + self.i2c.writeto_mem(ADDR, 4, b"TEST") + self.assertEqual(self.mem, bytearray(b"testTEST")) + + def test_write_wrap(self): + self.mem[:] = b"01234567" + self.i2c.writeto_mem(ADDR, 6, b"test") + self.assertEqual(self.mem, bytearray(b"st2345te")) + + @unittest.skipIf(sys.platform == "esp32", "write lengths larger than buffer unsupported") + def test_write_wrap_large(self): + self.mem[:] = b"01234567" + self.i2c.writeto_mem(ADDR, 0, b"testTESTmore") + self.assertEqual(self.mem, bytearray(b"moreTEST")) + + def test_read(self): + self.mem[:] = b"01234567" + self.assertEqual(self.i2c.readfrom_mem(ADDR, 0, 4), b"0123") + self.assertEqual(self.i2c.readfrom_mem(ADDR, 4, 4), b"4567") + + def test_read_wrap(self): + self.mem[:] = b"01234567" + self.assertEqual(self.i2c.readfrom_mem(ADDR, 0, 4), b"0123") + self.assertEqual(self.i2c.readfrom_mem(ADDR, 2, 4), b"2345") + self.assertEqual(self.i2c.readfrom_mem(ADDR, 6, 4), b"6701") + + @unittest.skipIf(sys.platform == "esp32", "read lengths larger than buffer unsupported") + def test_read_wrap_large(self): + self.mem[:] = b"01234567" + self.assertEqual(self.i2c.readfrom_mem(ADDR, 0, 12), b"012345670123") + + def test_write_read(self): + self.mem[:] = b"01234567" + self.assertEqual(self.i2c.writeto(ADDR, b"\x02"), 1) + self.assertEqual(self.i2c.readfrom(ADDR, 4), b"2345") + + @unittest.skipIf(sys.platform == "esp32", "read after read unsupported") + def test_write_read_read(self): + self.mem[:] = b"01234567" + self.assertEqual(self.i2c.writeto(ADDR, b"\x02"), 1) + self.assertEqual(self.i2c.readfrom(ADDR, 4), b"2345") + self.assertEqual(self.i2c.readfrom(ADDR, 4), b"7012") + + +@unittest.skipUnless(hasattr(I2CTarget, "IRQ_END_READ"), "IRQ unsupported") +class TestMemoryIRQ(unittest.TestCase): + @staticmethod + def irq_handler(i2c_target): + flags = i2c_target.irq().flags() + TestMemoryIRQ.events[TestMemoryIRQ.num_events] = flags + TestMemoryIRQ.events[TestMemoryIRQ.num_events + 1] = i2c_target.memaddr + TestMemoryIRQ.num_events += 2 + + @classmethod + def setUpClass(cls): + cls.mem = bytearray(8) + cls.events = [0] * 8 + cls.num_events = 0 + cls.i2c = SoftI2C(**args_controller) + cls.i2c_target = I2CTarget(*args_target, **kwargs_target, addr=ADDR, mem=cls.mem) + cls.i2c_target.irq(TestMemoryIRQ.irq_handler) + config_pull_up() + + @classmethod + def tearDownClass(cls): + cls.i2c_target.deinit() + + @unittest.skipIf(sys.platform == "esp32", "scan doesn't trigger IRQ_END_WRITE") + def test_scan(self): + TestMemoryIRQ.num_events = 0 + self.i2c.scan() + self.assertEqual(self.events[: self.num_events], [I2CTarget.IRQ_END_WRITE, 0]) + + def test_write(self): + TestMemoryIRQ.num_events = 0 + self.mem[:] = b"01234567" + self.i2c.writeto_mem(ADDR, 2, b"test") + self.assertEqual(self.mem, bytearray(b"01test67")) + self.assertEqual(self.events[: self.num_events], [I2CTarget.IRQ_END_WRITE, 2]) + + def test_read(self): + TestMemoryIRQ.num_events = 0 + self.mem[:] = b"01234567" + self.assertEqual(self.i2c.readfrom_mem(ADDR, 2, 4), b"2345") + self.assertEqual(self.events[: self.num_events], [I2CTarget.IRQ_END_READ, 2]) + + +@unittest.skipUnless(hasattr(I2CTarget, "IRQ_WRITE_REQ"), "IRQ unsupported") +@unittest.skipIf(sys.platform == "mimxrt", "not working") +@unittest.skipIf(sys.platform == "pyboard", "can't queue more than one byte") +@unittest.skipIf(sys.platform == "samd", "not working") +@unittest.skipIf(sys.platform == "zephyr", "must call readinto/write in IRQ handler") +class TestPolling(unittest.TestCase): + @staticmethod + def irq_handler(i2c_target, buf=bytearray(1)): + flags = i2c_target.irq().flags() + if flags & I2CTarget.IRQ_READ_REQ: + i2c_target.write(b"0123") + + @classmethod + def setUpClass(cls): + cls.i2c = SoftI2C(**args_controller) + cls.i2c_target = I2CTarget(*args_target, addr=ADDR) + cls.i2c_target.irq( + TestPolling.irq_handler, + I2CTarget.IRQ_WRITE_REQ | I2CTarget.IRQ_READ_REQ, + hard=True, + ) + config_pull_up() + + @classmethod + def tearDownClass(cls): + cls.i2c_target.deinit() + + def test_read(self): + # Can't write data up front, must wait until IRQ_READ_REQ. + # self.assertEqual(self.i2c_target.write(b"abcd"), 4) + self.assertEqual(self.i2c.readfrom(ADDR, 4), b"0123") + + def test_write(self): + # Can do the read outside the IRQ, but requires IRQ_WRITE_REQ trigger to be set. + self.assertEqual(self.i2c.writeto(ADDR, b"0123"), 4) + buf = bytearray(8) + self.assertEqual(self.i2c_target.readinto(buf), 4) + self.assertEqual(buf, b"0123\x00\x00\x00\x00") + + +@unittest.skipUnless(hasattr(I2CTarget, "IRQ_ADDR_MATCH_READ"), "IRQ unsupported") +class TestIRQ(unittest.TestCase): + @staticmethod + def irq_handler(i2c_target, buf=bytearray(1)): + flags = i2c_target.irq().flags() + TestIRQ.events[TestIRQ.num_events] = flags + TestIRQ.num_events += 1 + if flags & I2CTarget.IRQ_READ_REQ: + i2c_target.write(b"Y") + if flags & I2CTarget.IRQ_WRITE_REQ: + i2c_target.readinto(buf) + TestIRQ.events[TestIRQ.num_events] = buf[0] + TestIRQ.num_events += 1 + + @classmethod + def setUpClass(cls): + cls.events = [0] * 8 + cls.num_events = 0 + cls.i2c = SoftI2C(**args_controller) + cls.i2c_target = I2CTarget(*args_target, addr=ADDR) + cls.i2c_target.irq( + TestIRQ.irq_handler, + I2CTarget.IRQ_ADDR_MATCH_READ + | I2CTarget.IRQ_ADDR_MATCH_WRITE + | I2CTarget.IRQ_WRITE_REQ + | I2CTarget.IRQ_READ_REQ + | I2CTarget.IRQ_END_READ + | I2CTarget.IRQ_END_WRITE, + hard=True, + ) + config_pull_up() + + @classmethod + def tearDownClass(cls): + cls.i2c_target.deinit() + + def test_scan(self): + TestIRQ.num_events = 0 + self.i2c.scan() + self.assertEqual( + self.events[: self.num_events], + [ + I2CTarget.IRQ_ADDR_MATCH_WRITE, + I2CTarget.IRQ_END_WRITE, + ], + ) + + def test_write(self): + TestIRQ.num_events = 0 + self.i2c.writeto(ADDR, b"XYZ") + self.assertEqual( + self.events[: self.num_events], + [ + I2CTarget.IRQ_ADDR_MATCH_WRITE, + I2CTarget.IRQ_WRITE_REQ, + ord(b"X"), + I2CTarget.IRQ_WRITE_REQ, + ord(b"Y"), + I2CTarget.IRQ_WRITE_REQ, + ord(b"Z"), + I2CTarget.IRQ_END_WRITE, + ], + ) + + def test_read(self): + TestIRQ.num_events = 0 + self.assertEqual(self.i2c.readfrom(ADDR, 1), b"Y") + self.assertEqual( + self.events[: self.num_events], + [ + I2CTarget.IRQ_ADDR_MATCH_READ, + I2CTarget.IRQ_READ_REQ, + I2CTarget.IRQ_READ_REQ, + I2CTarget.IRQ_END_READ, + ], + ) + + def test_write_read(self): + TestIRQ.num_events = 0 + self.i2c.writeto(ADDR, b"X", False) + self.assertEqual(self.i2c.readfrom(ADDR, 1), b"Y") + self.assertEqual( + self.events[: self.num_events], + [ + I2CTarget.IRQ_ADDR_MATCH_WRITE, + I2CTarget.IRQ_WRITE_REQ, + ord(b"X"), + I2CTarget.IRQ_END_WRITE, + I2CTarget.IRQ_ADDR_MATCH_READ, + I2CTarget.IRQ_READ_REQ, + I2CTarget.IRQ_READ_REQ, + I2CTarget.IRQ_END_READ, + ], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/extmod_hardware/machine_pwm.py b/tests/extmod_hardware/machine_pwm.py new file mode 100644 index 00000000000..7d4f82fd2fe --- /dev/null +++ b/tests/extmod_hardware/machine_pwm.py @@ -0,0 +1,153 @@ +# Test machine.PWM, frequency and duty cycle (using machine.time_pulse_us). +# +# IMPORTANT: This test requires hardware connections: the PWM-output and pulse-input +# pins must be wired together (see the variable `pwm_loopback_pins`). + +try: + from machine import time_pulse_us, Pin, PWM +except ImportError: + print("SKIP") + raise SystemExit + +import machine, sys, time, unittest +from target_wiring import pwm_loopback_pins + +pwm_freq_limit = 1000000 +freq_margin_per_thousand = 0 +duty_margin_per_thousand = 0 +timing_margin_us = 5 + +# Slow MCUs cannot capture short pulses using `time_pulse_us` so limit the maximum PWM +# frequency tested on such targets. +if hasattr(machine, "freq"): + f = machine.freq() + if isinstance(f, tuple): + f = f[0] + if f <= 48_000_000: + pwm_freq_limit = 2_000 + elif f <= 64_000_000: + pwm_freq_limit = 5_000 + +# Tune test parameters based on the target. +if "esp32" in sys.platform: + freq_margin_per_thousand = 2 + duty_margin_per_thousand = 1 + timing_margin_us = 20 +elif "esp8266" in sys.platform: + pwm_freq_limit = 1_000 + duty_margin_per_thousand = 3 + timing_margin_us = 50 + + +# Test a specific frequency and duty cycle. +def _test_freq_duty(self, pulse_in, pwm, freq, duty_u16): + print("freq={:<5} duty_u16={:<5} :".format(freq, duty_u16), end="") + + # Check configured freq/duty_u16 is within error bound. + freq_error = abs(pwm.freq() - freq) * 1000 // freq + duty_error = abs(pwm.duty_u16() - duty_u16) * 1000 // (duty_u16 or 1) + print(" freq={} freq_er={}".format(pwm.freq(), freq_error), end="") + print(" duty={} duty_er={}".format(pwm.duty_u16(), duty_error), end="") + print(" :", end="") + self.assertLessEqual(freq_error, freq_margin_per_thousand) + self.assertLessEqual(duty_error, duty_margin_per_thousand) + + # Calculate expected timing. + expected_total_us = (1_000_000 + freq // 2) // freq + expected_high_us = (expected_total_us * duty_u16 + 65535 // 2) // 65535 + expected_low_us = expected_total_us - expected_high_us + expected_us = (expected_low_us, expected_high_us) + timeout = 2 * expected_total_us + + # Wait for output to settle. + time_pulse_us(pulse_in, 0, timeout) + time_pulse_us(pulse_in, 1, timeout) + + if duty_u16 == 0 or duty_u16 == 65535: + # Expect a constant output level. + no_pulse = ( + time_pulse_us(pulse_in, 0, timeout) < 0 and time_pulse_us(pulse_in, 1, timeout) < 0 + ) + self.assertTrue(no_pulse) + if expected_high_us == 0: + # Expect a constant low level. + self.assertEqual(pulse_in(), 0) + else: + # Expect a constant high level. + self.assertEqual(pulse_in(), 1) + else: + # Test timing of low and high pulse. + n_averaging = 10 + for level in (0, 1): + t = 0 + time_pulse_us(pulse_in, level, timeout) + for _ in range(n_averaging): + t += time_pulse_us(pulse_in, level, timeout) + t //= n_averaging + expected = expected_us[level] + print(" level={} timing_er={}".format(level, abs(t - expected)), end="") + self.assertLessEqual(abs(t - expected), timing_margin_us) + + print() + + +# Test a specific frequency with multiple duty cycles. +def _test_freq(self, freq): + print() + self.pwm.freq(freq) + for duty in (0, 10, 25, 50, 75, 90, 100): + duty_u16 = duty * 65535 // 100 + if sys.platform == "esp32": + # TODO why is this bit needed to get it working on esp32? + self.pwm.init(freq=freq, duty_u16=duty_u16) + time.sleep(0.1) + self.pwm.duty_u16(duty_u16) + _test_freq_duty(self, self.pulse_in, self.pwm, freq, duty_u16) + + +# Given a set of pins, this test class will test multiple frequencies and duty cycles. +class TestBase: + @classmethod + def setUpClass(cls): + print("set up pins:", cls.pwm_pin, cls.pulse_pin) + cls.pwm = PWM(cls.pwm_pin) + cls.pulse_in = Pin(cls.pulse_pin, Pin.IN) + + @classmethod + def tearDownClass(cls): + cls.pwm.deinit() + + def test_freq_50(self): + _test_freq(self, 50) + + def test_freq_100(self): + _test_freq(self, 100) + + def test_freq_500(self): + _test_freq(self, 500) + + def test_freq_1000(self): + _test_freq(self, 1000) + + @unittest.skipIf(pwm_freq_limit < 2000, "frequency too high") + def test_freq_2000(self): + _test_freq(self, 2000) + + @unittest.skipIf(pwm_freq_limit < 5000, "frequency too high") + def test_freq_5000(self): + _test_freq(self, 5000) + + @unittest.skipIf(pwm_freq_limit < 10000, "frequency too high") + def test_freq_10000(self): + _test_freq(self, 10000) + + +# Generate test classes, one for each set of pins to test. +for pwm, pulse in pwm_loopback_pins: + cls_name = "Test_{}_{}".format(pwm, pulse) + globals()[cls_name] = type( + cls_name, (TestBase, unittest.TestCase), {"pwm_pin": pwm, "pulse_pin": pulse} + ) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/extmod_hardware/machine_uart_irq_break.py b/tests/extmod_hardware/machine_uart_irq_break.py index f255e0f0e67..1832d9883ba 100644 --- a/tests/extmod_hardware/machine_uart_irq_break.py +++ b/tests/extmod_hardware/machine_uart_irq_break.py @@ -12,28 +12,7 @@ raise SystemExit import time, sys - -# Configure pins based on the target. -if "esp32" in sys.platform: - _machine = sys.implementation._machine - if "ESP32S2" in _machine or "ESP32C3" in _machine or "ESP32C6" in _machine: - print("SKIP") - raise SystemExit - # ESP32 needs separate UART instances for the test - recv_uart_id = 1 - recv_tx_pin = 14 - recv_rx_pin = 5 - send_uart_id = 2 - send_tx_pin = 4 - send_rx_pin = 12 -elif "rp2" in sys.platform: - recv_uart_id = 0 - send_uart_id = 0 - recv_tx_pin = "GPIO0" - recv_rx_pin = "GPIO1" -else: - print("Please add support for this test on this platform.") - raise SystemExit +from target_wiring import uart_loopback_args, uart_loopback_kwargs def irq(u): @@ -42,22 +21,17 @@ def irq(u): # Test that the IRQ is called for each break received. for bits_per_s in (2400, 9600, 57600): - recv_uart = UART(recv_uart_id, bits_per_s, tx=recv_tx_pin, rx=recv_rx_pin) - if recv_uart_id != send_uart_id: - send_uart = UART(send_uart_id, bits_per_s, tx=send_tx_pin, rx=send_rx_pin) - else: - send_uart = recv_uart - - recv_uart.irq(irq, recv_uart.IRQ_BREAK) + uart = UART(*uart_loopback_args, baudrate=bits_per_s, **uart_loopback_kwargs) + uart.irq(irq, uart.IRQ_BREAK) print("write", bits_per_s) for i in range(3): - send_uart.write(str(i)) - send_uart.flush() + uart.write(str(i)) + uart.flush() time.sleep_ms(10) - send_uart.sendbreak() + uart.sendbreak() time.sleep_ms(10) if "esp32" in sys.platform: # On esp32 a read is needed to read in the break byte. - recv_uart.read() + uart.read() print("done") diff --git a/tests/extmod_hardware/machine_uart_irq_rx.py b/tests/extmod_hardware/machine_uart_irq_rx.py index bf34900bd08..92fe218835d 100644 --- a/tests/extmod_hardware/machine_uart_irq_rx.py +++ b/tests/extmod_hardware/machine_uart_irq_rx.py @@ -13,40 +13,14 @@ import time, sys -byte_by_byte = False -# Configure pins based on the target. -if "esp32" in sys.platform: - uart_id = 1 - tx_pin = 4 - rx_pin = 5 -elif "pyboard" in sys.platform: - uart_id = 4 - tx_pin = None # PA0 - rx_pin = None # PA1 -elif "samd" in sys.platform and "ItsyBitsy M0" in sys.implementation._machine: - uart_id = 0 - tx_pin = "D1" - rx_pin = "D0" - byte_by_byte = True -elif "samd" in sys.platform and "ItsyBitsy M4" in sys.implementation._machine: - uart_id = 3 - tx_pin = "D1" - rx_pin = "D0" -elif "nrf" in sys.platform: - uart_id = 0 - tx_pin = None - rx_pin = None -elif "renesas-ra" in sys.platform: - uart_id = 9 - tx_pin = None # P602 @ RA6M2 - rx_pin = None # P601 @ RA6M2 -elif "CC3200" in sys.implementation._machine: +if "CC3200" in sys.implementation._machine: # CC3200 doesn't work because it's too slow and has an allocation error in the handler. print("SKIP") raise SystemExit -else: - print("Please add support for this test on this platform.") - raise SystemExit + +from target_wiring import uart_loopback_args, uart_loopback_kwargs + +byte_by_byte = "ItsyBitsy M0" in sys.implementation._machine def irq(u): @@ -58,11 +32,7 @@ def irq(u): # Test that the IRQ is called for each byte received. # Use slow baudrates so that the IRQ has time to run. for bits_per_s in (2400, 9600): - if tx_pin is None: - uart = UART(uart_id, bits_per_s) - else: - uart = UART(uart_id, bits_per_s, tx=tx_pin, rx=rx_pin) - + uart = UART(*uart_loopback_args, baudrate=bits_per_s, **uart_loopback_kwargs) uart.irq(irq, uart.IRQ_RX) print("write", bits_per_s) diff --git a/tests/extmod_hardware/machine_uart_irq_rxidle.py b/tests/extmod_hardware/machine_uart_irq_rxidle.py index 182ab24ebe0..40f781d0788 100644 --- a/tests/extmod_hardware/machine_uart_irq_rxidle.py +++ b/tests/extmod_hardware/machine_uart_irq_rxidle.py @@ -12,59 +12,38 @@ raise SystemExit import time, sys +from target_wiring import uart_loopback_args, uart_loopback_kwargs -# Configure pins based on the target. -if "esp32" in sys.platform: - uart_id = 1 - tx_pin = 4 - rx_pin = 5 -elif "mimxrt" in sys.platform: - uart_id = 1 - tx_pin = None -elif "pyboard" in sys.platform: - uart_id = 4 - tx_pin = None # PA0 - rx_pin = None # PA1 -elif "renesas-ra" in sys.platform: - uart_id = 9 - tx_pin = None # P602 @ RA6M2 - rx_pin = None # P601 @ RA6M2 -elif "rp2" in sys.platform: - uart_id = 0 - tx_pin = "GPIO0" - rx_pin = "GPIO1" -elif "samd" in sys.platform and "ItsyBitsy M0" in sys.implementation._machine: - uart_id = 0 - tx_pin = "D1" - rx_pin = "D0" - byte_by_byte = True -elif "samd" in sys.platform and "ItsyBitsy M4" in sys.implementation._machine: - uart_id = 3 - tx_pin = "D1" - rx_pin = "D0" -else: - print("Please add support for this test on this platform.") - raise SystemExit +# Target tuning options. +tune_wait_initial_rxidle = sys.platform == "pyboard" def irq(u): print("IRQ_RXIDLE:", bool(u.irq().flags() & u.IRQ_RXIDLE), "data:", u.read()) -text = "12345678" +text = ("12345678", "abcdefgh") # Test that the IRQ is called for each set of byte received. for bits_per_s in (2400, 9600, 115200): - if tx_pin is None: - uart = UART(uart_id, bits_per_s) - else: - uart = UART(uart_id, bits_per_s, tx=tx_pin, rx=rx_pin) + print("========") + print("bits_per_s:", bits_per_s) + + uart = UART(*uart_loopback_args, baudrate=bits_per_s, **uart_loopback_kwargs) + + # Ignore a possible initial RXIDLE condition after creating UART. + if tune_wait_initial_rxidle: + uart.irq(lambda _: None, uart.IRQ_RXIDLE) + time.sleep_ms(10) + # Configure desired IRQ. uart.irq(irq, uart.IRQ_RXIDLE) - print("write", bits_per_s) - uart.write(text) - uart.flush() - print("ready") - time.sleep_ms(100) - print("done") + for i in range(2): + # Write data and wait for IRQ. + print("write") + uart.write(text[i]) + uart.flush() + print("ready") + time.sleep_ms(100) + print("done") diff --git a/tests/extmod_hardware/machine_uart_irq_rxidle.py.exp b/tests/extmod_hardware/machine_uart_irq_rxidle.py.exp index ce1890a06a4..f3c7497e4ce 100644 --- a/tests/extmod_hardware/machine_uart_irq_rxidle.py.exp +++ b/tests/extmod_hardware/machine_uart_irq_rxidle.py.exp @@ -1,12 +1,30 @@ -write 2400 +======== +bits_per_s: 2400 +write ready IRQ_RXIDLE: True data: b'12345678' done -write 9600 +write +ready +IRQ_RXIDLE: True data: b'abcdefgh' +done +======== +bits_per_s: 9600 +write ready IRQ_RXIDLE: True data: b'12345678' done -write 115200 +write +ready +IRQ_RXIDLE: True data: b'abcdefgh' +done +======== +bits_per_s: 115200 +write ready IRQ_RXIDLE: True data: b'12345678' done +write +ready +IRQ_RXIDLE: True data: b'abcdefgh' +done diff --git a/tests/feature_check/float.py b/tests/feature_check/float.py deleted file mode 100644 index d6d2a99d242..00000000000 --- a/tests/feature_check/float.py +++ /dev/null @@ -1,13 +0,0 @@ -# detect how many bits of precision the floating point implementation has - -try: - float -except NameError: - print(0) -else: - if float("1.0000001") == float("1.0"): - print(30) - elif float("1e300") == float("inf"): - print(32) - else: - print(64) diff --git a/tests/feature_check/float.py.exp b/tests/feature_check/float.py.exp deleted file mode 100644 index 900731ffd51..00000000000 --- a/tests/feature_check/float.py.exp +++ /dev/null @@ -1 +0,0 @@ -64 diff --git a/tests/feature_check/inlineasm_rv32.py b/tests/feature_check/inlineasm_rv32.py new file mode 100644 index 00000000000..21dd103b6c3 --- /dev/null +++ b/tests/feature_check/inlineasm_rv32.py @@ -0,0 +1,9 @@ +# check if RISC-V 32 inline asm is supported + + +@micropython.asm_rv32 +def f(): + add(a0, a0, a0) + + +print("rv32") diff --git a/tests/feature_check/inlineasm_rv32.py.exp b/tests/feature_check/inlineasm_rv32.py.exp new file mode 100644 index 00000000000..5eecf09c224 --- /dev/null +++ b/tests/feature_check/inlineasm_rv32.py.exp @@ -0,0 +1 @@ +rv32 diff --git a/tests/feature_check/inlineasm_rv32_zba.py b/tests/feature_check/inlineasm_rv32_zba.py new file mode 100644 index 00000000000..81228819042 --- /dev/null +++ b/tests/feature_check/inlineasm_rv32_zba.py @@ -0,0 +1,10 @@ +# check if RISC-V 32 inline asm supported Zba opcodes + + +@micropython.asm_rv32 +def f(): + sh1add(a0, a0, a0) + + +f() +print("rv32_zba") diff --git a/tests/feature_check/inlineasm_rv32_zba.py.exp b/tests/feature_check/inlineasm_rv32_zba.py.exp new file mode 100644 index 00000000000..fde22f5f400 --- /dev/null +++ b/tests/feature_check/inlineasm_rv32_zba.py.exp @@ -0,0 +1 @@ +rv32_zba diff --git a/tests/feature_check/inlineasm_thumb.py b/tests/feature_check/inlineasm_thumb.py new file mode 100644 index 00000000000..321eab0e2f8 --- /dev/null +++ b/tests/feature_check/inlineasm_thumb.py @@ -0,0 +1,9 @@ +# check if Thumb inline asm is supported + + +@micropython.asm_thumb +def f(): + nop() + + +print("thumb") diff --git a/tests/feature_check/inlineasm_thumb.py.exp b/tests/feature_check/inlineasm_thumb.py.exp new file mode 100644 index 00000000000..bb48e1a2f03 --- /dev/null +++ b/tests/feature_check/inlineasm_thumb.py.exp @@ -0,0 +1 @@ +thumb diff --git a/tests/feature_check/inlineasm_xtensa.py b/tests/feature_check/inlineasm_xtensa.py new file mode 100644 index 00000000000..2a24d39973c --- /dev/null +++ b/tests/feature_check/inlineasm_xtensa.py @@ -0,0 +1,9 @@ +# check if Xtensa inline asm is supported + + +@micropython.asm_xtensa +def f(): + ret_n() + + +print("xtensa") diff --git a/tests/feature_check/inlineasm_xtensa.py.exp b/tests/feature_check/inlineasm_xtensa.py.exp new file mode 100644 index 00000000000..036142c5097 --- /dev/null +++ b/tests/feature_check/inlineasm_xtensa.py.exp @@ -0,0 +1 @@ +xtensa diff --git a/tests/feature_check/int_64.py b/tests/feature_check/int_64.py new file mode 100644 index 00000000000..4d053782ca8 --- /dev/null +++ b/tests/feature_check/int_64.py @@ -0,0 +1,2 @@ +# Check whether 64-bit long integers are supported +print(1 << 62) diff --git a/tests/feature_check/int_64.py.exp b/tests/feature_check/int_64.py.exp new file mode 100644 index 00000000000..aef5454e662 --- /dev/null +++ b/tests/feature_check/int_64.py.exp @@ -0,0 +1 @@ +4611686018427387904 diff --git a/tests/feature_check/io_module.py b/tests/feature_check/io_module.py deleted file mode 100644 index 9094e605316..00000000000 --- a/tests/feature_check/io_module.py +++ /dev/null @@ -1,6 +0,0 @@ -try: - import io - - print("io") -except ImportError: - print("no") diff --git a/tests/feature_check/io_module.py.exp b/tests/feature_check/io_module.py.exp deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/feature_check/repl_emacs_check.py.exp b/tests/feature_check/repl_emacs_check.py.exp index 82a4e28ee4f..2dfb2da58b8 100644 --- a/tests/feature_check/repl_emacs_check.py.exp +++ b/tests/feature_check/repl_emacs_check.py.exp @@ -1,7 +1,7 @@ MicroPython \.\+ version -Use \.\+ +Type "help()" for more information. >>> # Check for emacs keys in REPL >>> t = \.\+ >>> t == 2 True ->>> +>>> \$ diff --git a/tests/feature_check/repl_words_move_check.py.exp b/tests/feature_check/repl_words_move_check.py.exp index 82a4e28ee4f..2dfb2da58b8 100644 --- a/tests/feature_check/repl_words_move_check.py.exp +++ b/tests/feature_check/repl_words_move_check.py.exp @@ -1,7 +1,7 @@ MicroPython \.\+ version -Use \.\+ +Type "help()" for more information. >>> # Check for emacs keys in REPL >>> t = \.\+ >>> t == 2 True ->>> +>>> \$ diff --git a/tests/feature_check/target_info.py b/tests/feature_check/target_info.py index df89496708a..ed91b27b775 100644 --- a/tests/feature_check/target_info.py +++ b/tests/feature_check/target_info.py @@ -4,6 +4,7 @@ import sys +platform = getattr(sys, "platform", "minimal") sys_mpy = getattr(sys.implementation, "_mpy", 0) arch = [ None, @@ -18,5 +19,27 @@ "xtensa", "xtensawin", "rv32imc", -][sys_mpy >> 10] -print(arch) + "rv64imc", +][(sys_mpy >> 10) & 0x0F] +arch_flags = sys_mpy >> 16 +build = getattr(sys.implementation, "_build", "unknown") +thread = getattr(sys.implementation, "_thread", None) + +# Detect how many bits of precision the floating point implementation has. +try: + if float("1.0000001") == float("1.0"): + float_prec = 30 + elif float("1e300") == float("inf"): + float_prec = 32 + else: + float_prec = 64 +except NameError: + float_prec = 0 + +# Detect the error reporting level (based on the length of the raised exception message). +try: + (lambda: 0)(0) +except TypeError as er: + error_reporting = {0: "none", 27: "terse", 54: "normal", 56: "detailed"}[len(er.value)] + +print(platform, arch, arch_flags, build, thread, float_prec, len("α") == 1, error_reporting) diff --git a/tests/feature_check/tstring.py b/tests/feature_check/tstring.py new file mode 100644 index 00000000000..05322b2ae61 --- /dev/null +++ b/tests/feature_check/tstring.py @@ -0,0 +1,5 @@ +# check whether t-strings (PEP-750) are supported + +a = 1 +t = t"a={a}" +print("tstring") diff --git a/tests/feature_check/tstring.py.exp b/tests/feature_check/tstring.py.exp new file mode 100644 index 00000000000..ba42b0ec666 --- /dev/null +++ b/tests/feature_check/tstring.py.exp @@ -0,0 +1 @@ +tstring diff --git a/tests/float/cmath_fun.py b/tests/float/cmath_fun.py index 39011733b02..0037d7c6559 100644 --- a/tests/float/cmath_fun.py +++ b/tests/float/cmath_fun.py @@ -51,6 +51,9 @@ print("%.5g" % ret) elif type(ret) == tuple: print("%.5g %.5g" % ret) + elif f_name == "exp": + # exp amplifies REPR_C inaccuracies, so we need to check one digit less + print("complex(%.4g, %.4g)" % (real, ret.imag)) else: # some test (eg cmath.sqrt(-0.5)) disagree with CPython with tiny real part real = ret.real diff --git a/tests/float/complex1.py b/tests/float/complex1.py index f4107a1390f..4decea2ac6b 100644 --- a/tests/float/complex1.py +++ b/tests/float/complex1.py @@ -12,13 +12,14 @@ print(complex("1+j")) print(complex("1+2j")) print(complex("-1-2j")) +print(complex("-1+2j")) print(complex("+1-2j")) print(complex(" -1-2j ")) print(complex(" +1-2j ")) +print(complex(" -1+2j ")) print(complex("nanj")) print(complex("nan-infj")) print(complex(1, 2)) -print(complex(1j, 2j)) # unary ops print(bool(1j)) diff --git a/tests/float/complex1_micropython.py b/tests/float/complex1_micropython.py new file mode 100644 index 00000000000..6b92f593ef2 --- /dev/null +++ b/tests/float/complex1_micropython.py @@ -0,0 +1,6 @@ +# test basic complex number functionality + +# CPython 3.14 marks this constructor as deprecated, but it is still currently +# supported by MicroPython. + +print(complex(1j, 2j)) diff --git a/tests/float/complex1_micropython.py.exp b/tests/float/complex1_micropython.py.exp new file mode 100644 index 00000000000..1defdb822cd --- /dev/null +++ b/tests/float/complex1_micropython.py.exp @@ -0,0 +1 @@ +(-2+1j) diff --git a/tests/float/float_array.py b/tests/float/float_array.py index 3d128da8381..cfff3b220c6 100644 --- a/tests/float/float_array.py +++ b/tests/float/float_array.py @@ -19,4 +19,10 @@ def test(a): test(array("f")) test(array("d")) -print("{:.4f}".format(array("f", bytes(array("I", [0x3DCCCCCC])))[0])) +# hand-crafted floats, including non-standard nan +for float_hex in (0x3DCCCCCC, 0x7F800024, 0x7FC00004): + f = array("f", bytes(array("I", [float_hex])))[0] + if type(f) is float: + print("{:.4e}".format(f)) + else: + print(f) diff --git a/tests/float/float_format.py b/tests/float/float_format.py index 98ed0eb096f..0eb8b232b06 100644 --- a/tests/float/float_format.py +++ b/tests/float/float_format.py @@ -2,14 +2,25 @@ # general rounding for val in (116, 1111, 1234, 5010, 11111): - print("%.0f" % val) - print("%.1f" % val) - print("%.3f" % val) + print("Test on %d / 1000:" % val) + for fmt in ("%.5e", "%.3e", "%.1e", "%.0e", "%.3f", "%.1f", "%.0f", "%.3g", "%.1g", "%.0g"): + print(fmt, fmt % (val / 1000)) + +# make sure round-up to the next unit is handled properly +for val in range(4, 9): + divi = 10**val + print("Test on 99994 / (10 ** %d):" % val) + for fmt in ("%.5e", "%.3e", "%.1e", "%.0e", "%.3f", "%.1f", "%.0f", "%.3g", "%.1g", "%.0g"): + print(fmt, fmt % (99994 / divi)) # make sure rounding is done at the correct precision for prec in range(8): print(("%%.%df" % prec) % 6e-5) +# make sure trailing zeroes are added properly +for prec in range(8): + print(("%%.%df" % prec) % 1e19) + # check certain cases that had a digit value of 10 render as a ":" character print("%.2e" % float("9" * 51 + "e-39")) print("%.2e" % float("9" * 40 + "e-21")) diff --git a/tests/float/float_format_accuracy.py b/tests/float/float_format_accuracy.py new file mode 100644 index 00000000000..f9467f9c05d --- /dev/null +++ b/tests/float/float_format_accuracy.py @@ -0,0 +1,73 @@ +# Test accuracy of `repr` conversions. +# This test also increases code coverage for corner cases. + +try: + import array, math, random +except ImportError: + print("SKIP") + raise SystemExit + +# The largest errors come from seldom used very small numbers, near the +# limit of the representation. So we keep them out of this test to keep +# the max relative error display useful. +if float("1e-100") == 0.0: + # single-precision + float_type = "f" + float_size = 4 + # testing range + min_expo = -96 # i.e. not smaller than 1.0e-29 + # Expected results (given >=50'000 samples): + # - MICROPY_FLTCONV_IMPL_EXACT: 100% exact conversions + # - MICROPY_FLTCONV_IMPL_APPROX: >=98.53% exact conversions, max relative error <= 1.01e-7 + min_success = 0.980 # with only 1200 samples, the success rate is lower + max_rel_err = 1.1e-7 + # REPR_C is typically used with FORMAT_IMPL_BASIC, which has a larger error + is_REPR_C = float("1.0000001") == float("1.0") + if is_REPR_C: # REPR_C + min_success = 0.83 + max_rel_err = 5.75e-07 +else: + # double-precision + float_type = "d" + float_size = 8 + # testing range + min_expo = -845 # i.e. not smaller than 1.0e-254 + # Expected results (given >=200'000 samples): + # - MICROPY_FLTCONV_IMPL_EXACT: 100% exact conversions + # - MICROPY_FLTCONV_IMPL_APPROX: >=99.83% exact conversions, max relative error <= 2.7e-16 + min_success = 0.997 # with only 1200 samples, the success rate is lower + max_rel_err = 2.7e-16 + + +# Deterministic pseudorandom generator. Designed to be uniform +# on mantissa values and exponents, not on the represented number +def pseudo_randfloat(): + rnd_buff = bytearray(float_size) + for _ in range(float_size): + rnd_buff[_] = random.getrandbits(8) + return array.array(float_type, rnd_buff)[0] + + +random.seed(42) +stats = 0 +N = 1200 +max_err = 0 +for _ in range(N): + f = pseudo_randfloat() + while type(f) is not float or math.isinf(f) or math.isnan(f) or math.frexp(f)[1] <= min_expo: + f = pseudo_randfloat() + + str_f = repr(f) + f2 = float(str_f) + if f2 == f: + stats += 1 + else: + error = abs((f2 - f) / f) + if max_err < error: + max_err = error + +print(N, "values converted") +if stats / N >= min_success and max_err <= max_rel_err: + print("float format accuracy OK") +else: + print("FAILED: repr rate=%.3f%% max_err=%.3e" % (100 * stats / N, max_err)) diff --git a/tests/float/float_format_ints.py b/tests/float/float_format_ints.py index df4444166c5..1f221bd5e79 100644 --- a/tests/float/float_format_ints.py +++ b/tests/float/float_format_ints.py @@ -12,14 +12,49 @@ print(title, "with format", f_fmt, "gives", f_fmt.format(f)) print(title, "with format", g_fmt, "gives", g_fmt.format(f)) +# The tests below check border cases involving all mantissa bits. +# In case of REPR_C, where the mantissa is missing two bits, the +# the string representation for such numbers might not always be exactly +# the same but nevertheless be correct, so we must allow a few exceptions. +is_REPR_C = float("1.0000001") == float("1.0") + # 16777215 is 2^24 - 1, the largest integer that can be completely held # in a float32. -print("{:f}".format(16777215)) +val_str = "{:f}".format(16777215) + +# When using REPR_C, 16777215.0 is the same as 16777212.0 or 16777214.4 +# (depending on the implementation of pow() function, the result may differ) +if is_REPR_C and (val_str == "16777212.000000" or val_str == "16777214.400000"): + val_str = "16777215.000000" + +print(val_str) + # 4294967040 = 16777215 * 128 is the largest integer that is exactly # represented by a float32 and that will also fit within a (signed) int32. # The upper bound of our integer-handling code is actually double this, # but that constant might cause trouble on systems using 32 bit ints. -print("{:f}".format(2147483520)) +val_str = "{:f}".format(2147483520) + +# When using FLOAT_IMPL_FLOAT, 2147483520.0 == 2147483500.0 +# Both representations are valid, the second being "simpler" +is_float32 = float("1e300") == float("inf") +if is_float32 and val_str == "2147483500.000000": + val_str = "2147483520.000000" + +# When using REPR_C, 2147483520.0 is the same as 2147483200.0 +# Both representations are valid, the second being "simpler" +if is_REPR_C and val_str == "2147483200.000000": + val_str = "2147483520.000000" + +# When using REPR_C, x86 and clang, 2147483520.0 is the same +# as 2147483100.0, the second being "simple" but rounded differently +# due to x87 extra precision on intermediates. +# Both representations are valid. +if is_REPR_C and val_str == "2147483100.000000": + val_str = "2147483520.000000" + +print(val_str) + # Very large positive integers can be a test for precision and resolution. # This is a weird way to represent 1e38 (largest power of 10 for float32). print("{:.6e}".format(float("9" * 30 + "e8"))) diff --git a/tests/float/float_parse.py b/tests/float/float_parse.py index de27c33e7be..6131da0a63a 100644 --- a/tests/float/float_parse.py +++ b/tests/float/float_parse.py @@ -31,6 +31,9 @@ print(float("1e18446744073709551621")) print(float("1e-18446744073709551621")) +# mantissa overflow while processing deferred trailing zeros +print(float("10000000000000000000001")) + # check small decimals are as close to their true value as possible for n in range(1, 10): print(float("0.%u" % n) == n / 10) diff --git a/tests/float/float_parse_doubleprec.py b/tests/float/float_parse_doubleprec.py index 81fcadcee88..c1b0b4823b0 100644 --- a/tests/float/float_parse_doubleprec.py +++ b/tests/float/float_parse_doubleprec.py @@ -19,3 +19,9 @@ print(float("1.00000000000000000000e-307")) print(float("10.0000000000000000000e-308")) print(float("100.000000000000000000e-309")) + +# ensure repr() adds an extra digit when needed for accurate parsing +print(float(repr(float("2.0") ** 100)) == float("2.0") ** 100) + +# ensure repr does not add meaningless extra digits (1.234999999999) +print(repr(1.2345)) diff --git a/tests/float/float_struct_e.py b/tests/float/float_struct_e.py index 403fbc5db4c..ba4134f3393 100644 --- a/tests/float/float_struct_e.py +++ b/tests/float/float_struct_e.py @@ -32,7 +32,7 @@ for i in (j, -j): x = struct.pack("", "=", "^"): for fill in ("", " ", "0", "@"): for sign in ("", "+", "-", " "): - # An empty precision defaults to 6, but when uPy is + # An empty precision defaults to 6, but when MicroPython is # configured to use a float, we can only use a # precision of 6 with numbers less than 10 and still # get results that compare to CPython (which uses @@ -164,7 +164,7 @@ def test_fmt(conv, fill, alignment, sign, prefix, width, precision, type, arg): for alignment in ("", "<", ">", "=", "^"): for fill in ("", " ", "0", "@"): for sign in ("", "+", "-", " "): - # An empty precision defaults to 6, but when uPy is + # An empty precision defaults to 6, but when MicroPython is # configured to use a float, we can only use a # precision of 6 with numbers less than 10 and still # get results that compare to CPython (which uses diff --git a/tests/float/string_format_fp30.py b/tests/float/string_format_fp30.py deleted file mode 100644 index 5f0b213daa3..00000000000 --- a/tests/float/string_format_fp30.py +++ /dev/null @@ -1,42 +0,0 @@ -def test(fmt, *args): - print("{:8s}".format(fmt) + ">" + fmt.format(*args) + "<") - - -test("{:10.4}", 123.456) -test("{:10.4e}", 123.456) -test("{:10.4e}", -123.456) -# test("{:10.4f}", 123.456) -# test("{:10.4f}", -123.456) -test("{:10.4g}", 123.456) -test("{:10.4g}", -123.456) -test("{:10.4n}", 123.456) -test("{:e}", 100) -test("{:f}", 200) -test("{:g}", 300) - -test("{:10.4E}", 123.456) -test("{:10.4E}", -123.456) -# test("{:10.4F}", 123.456) -# test("{:10.4F}", -123.456) -test("{:10.4G}", 123.456) -test("{:10.4G}", -123.456) - -test("{:06e}", float("inf")) -test("{:06e}", float("-inf")) -test("{:06e}", float("nan")) - -# The following fails right now -# test("{:10.1}", 0.0) - -print("%.0f" % (1.750000 % 0.08333333333)) -# Below isn't compatible with single-precision float -# print("%.1f" % (1.750000 % 0.08333333333)) -# print("%.2f" % (1.750000 % 0.08333333333)) -# print("%.12f" % (1.750000 % 0.08333333333)) - -# tests for errors in format string - -try: - "{:10.1b}".format(0.0) -except ValueError: - print("ValueError") diff --git a/tests/float/string_format_modulo.py b/tests/float/string_format_modulo.py index 3c206b73935..55314643351 100644 --- a/tests/float/string_format_modulo.py +++ b/tests/float/string_format_modulo.py @@ -6,7 +6,7 @@ print("%u" % 1.0) # these 3 have different behaviour in Python 3.x versions -# uPy raises a TypeError, following Python 3.5 (earlier versions don't) +# MicroPython raises a TypeError, following Python 3.5 (earlier versions don't) # print("%x" % 18.0) # print("%o" % 18.0) # print("%X" % 18.0) diff --git a/tests/float/string_format_modulo3.py b/tests/float/string_format_modulo3.py index f9d9c43cdf4..f8aeeda20f2 100644 --- a/tests/float/string_format_modulo3.py +++ b/tests/float/string_format_modulo3.py @@ -1,3 +1,3 @@ -# uPy and CPython outputs differ for the following +# Test corner cases where MicroPython and CPython outputs used to differ in the past print("%.1g" % -9.9) # round up 'g' with '-' sign print("%.2g" % 99.9) # round up diff --git a/tests/float/string_format_modulo3.py.exp b/tests/float/string_format_modulo3.py.exp deleted file mode 100644 index 71432b34045..00000000000 --- a/tests/float/string_format_modulo3.py.exp +++ /dev/null @@ -1,2 +0,0 @@ --10 -100 diff --git a/tests/frozen/README.md b/tests/frozen/README.md deleted file mode 100644 index bd786d5a3c4..00000000000 --- a/tests/frozen/README.md +++ /dev/null @@ -1,2 +0,0 @@ -This is a .mpy built against the current .mpy version that can be used to test -freezing without a dependency on mpy-cross. diff --git a/tests/frozen/frozentest.mpy b/tests/frozen/frozentest.mpy deleted file mode 100644 index 99581617ac3..00000000000 Binary files a/tests/frozen/frozentest.mpy and /dev/null differ diff --git a/tests/import/builtin_ext.py b/tests/import/builtin_ext.py index 87465f1d593..aecbbb2639a 100644 --- a/tests/import/builtin_ext.py +++ b/tests/import/builtin_ext.py @@ -1,3 +1,9 @@ +try: + import uos, utime +except ImportError: + print("SKIP") + raise SystemExit + # Verify that sys is a builtin. import sys diff --git a/tests/import/builtin_import.py b/tests/import/builtin_import.py index 734498d1be4..364b0bae912 100644 --- a/tests/import/builtin_import.py +++ b/tests/import/builtin_import.py @@ -20,3 +20,12 @@ __import__("xyz", None, None, None, -1) except ValueError: print("ValueError") + +# globals is not checked for level=0 +__import__("builtins", "globals") + +# globals must be a dict (or None) for level>0 +try: + __import__("builtins", "globals", None, None, 1) +except TypeError: + print("TypeError") diff --git a/tests/import/import_broken.py b/tests/import/import_broken.py index 3c7cf4a4985..440922157ef 100644 --- a/tests/import/import_broken.py +++ b/tests/import/import_broken.py @@ -1,3 +1,9 @@ +try: + Exception.__class__ +except AttributeError: + print("SKIP") + raise SystemExit + import sys, pkg # Modules we import are usually added to sys.modules. diff --git a/tests/import/import_file.py b/tests/import/import_file.py index 90ec4e41e77..4cf307641c6 100644 --- a/tests/import/import_file.py +++ b/tests/import/import_file.py @@ -1,3 +1,7 @@ +if "__file__" not in globals(): + print("SKIP") + raise SystemExit + import import1b print(import1b.__file__) diff --git a/tests/import/import_override.py b/tests/import/import_override.py index 029ebe54c1f..0144e78cb9f 100644 --- a/tests/import/import_override.py +++ b/tests/import/import_override.py @@ -2,6 +2,10 @@ def custom_import(name, globals, locals, fromlist, level): + # CPython always tries to import _io, so just let that through as-is. + if name == "_io": + return orig_import(name, globals, locals, fromlist, level) + print("import", name, fromlist, level) class M: diff --git a/tests/import/import_override.py.exp b/tests/import/import_override.py.exp deleted file mode 100644 index 365248da6d8..00000000000 --- a/tests/import/import_override.py.exp +++ /dev/null @@ -1,2 +0,0 @@ -import import1b None 0 -456 diff --git a/tests/import/import_override2.py b/tests/import/import_override2.py new file mode 100644 index 00000000000..25aac44fe98 --- /dev/null +++ b/tests/import/import_override2.py @@ -0,0 +1,18 @@ +# test overriding __import__ combined with importing from the filesystem + + +def custom_import(name, globals, locals, fromlist, level): + if level > 0: + print("import", name, fromlist, level) + return orig_import(name, globals, locals, fromlist, level) + + +orig_import = __import__ +try: + __import__("builtins").__import__ = custom_import +except AttributeError: + print("SKIP") + raise SystemExit + +# import calls __import__ behind the scenes +import pkg7.subpkg1.subpkg2.mod3 diff --git a/tests/import/import_pkg7.py.exp b/tests/import/import_pkg7.py.exp deleted file mode 100644 index 8f21a615f6a..00000000000 --- a/tests/import/import_pkg7.py.exp +++ /dev/null @@ -1,8 +0,0 @@ -pkg __name__: pkg7 -pkg __name__: pkg7.subpkg1 -pkg __name__: pkg7.subpkg1.subpkg2 -mod1 -mod2 -mod1.foo -mod2.bar -ImportError diff --git a/tests/import/import_pkg9.py b/tests/import/import_pkg9.py index 4de028494f1..c2e0b618b69 100644 --- a/tests/import/import_pkg9.py +++ b/tests/import/import_pkg9.py @@ -13,4 +13,4 @@ import pkg9.mod2 pkg9.mod1() -print(pkg9.mod2.__name__, type(pkg9.mod2).__name__) +print(pkg9.mod2.__name__, type(pkg9.mod2)) diff --git a/tests/import/import_star.py b/tests/import/import_star.py new file mode 100644 index 00000000000..2cb21b877d7 --- /dev/null +++ b/tests/import/import_star.py @@ -0,0 +1,59 @@ +# test `from package import *` conventions, including __all__ support +# +# This test requires MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_BASIC_FEATURES + +try: + next(iter([]), 42) +except TypeError: + # 2-argument version of next() not supported + # we are probably not at MICROPY_CONFIG_ROM_LEVEL_BASIC_FEATURES + print("SKIP") + raise SystemExit + +# 1. test default visibility +from pkgstar_default import * + +print("visibleFun" in globals()) +print("VisibleClass" in globals()) +print("_hiddenFun" in globals()) +print("_HiddenClass" in globals()) +print(visibleFun()) + +# 2. test explicit visibility as defined by __all__ (as an array) +from pkgstar_all_array import * + +print("publicFun" in globals()) +print("PublicClass" in globals()) +print("unlistedFun" in globals()) +print("UnlistedClass" in globals()) +print("_privateFun" in globals()) +print("_PrivateClass" in globals()) +print(publicFun()) +# test dynamic import as used in asyncio +print("dynamicFun" in globals()) +print(dynamicFun()) + +# 3. test explicit visibility as defined by __all__ (as an tuple) +from pkgstar_all_tuple import * + +print("publicFun2" in globals()) +print("PublicClass2" in globals()) +print("unlistedFun2" in globals()) +print("UnlistedClass2" in globals()) +print(publicFun2()) + +# 4. test reporting of missing entries in __all__ +try: + from pkgstar_all_miss import * + + print("missed detection of incorrect __all__ definition") +except AttributeError as er: + print("AttributeError triggered for bad __all__ definition") + +# 5. test reporting of invalid __all__ definition +try: + from pkgstar_all_inval import * + + print("missed detection of incorrect __all__ definition") +except TypeError as er: + print("TypeError triggered for bad __all__ definition") diff --git a/tests/import/import_star_error.py b/tests/import/import_star_error.py index 9e1757b6ef5..73d9c863d6f 100644 --- a/tests/import/import_star_error.py +++ b/tests/import/import_star_error.py @@ -1,5 +1,10 @@ # test errors with import * +if not hasattr(object, "__init__"): + # target doesn't have MICROPY_CPYTHON_COMPAT enabled, so doesn't check for "import *" + print("SKIP") + raise SystemExit + # 'import *' is not allowed in function scope try: exec("def foo(): from x import *") diff --git a/tests/import/module_getattr.py.exp b/tests/import/module_getattr.py.exp deleted file mode 100644 index bc59c12aa16..00000000000 --- a/tests/import/module_getattr.py.exp +++ /dev/null @@ -1 +0,0 @@ -False diff --git a/tests/import/pkg7/subpkg1/subpkg2/mod3.py b/tests/import/pkg7/subpkg1/subpkg2/mod3.py index b0f4279fcf5..f7f4c1e65f4 100644 --- a/tests/import/pkg7/subpkg1/subpkg2/mod3.py +++ b/tests/import/pkg7/subpkg1/subpkg2/mod3.py @@ -5,7 +5,9 @@ print(bar) # attempted relative import beyond top-level package +# On older versions of CPython (eg 3.8) this is a ValueError, but on +# newer CPython (eg 3.11) and MicroPython it's an ImportError. try: from .... import mod1 -except ImportError: +except (ImportError, ValueError): print("ImportError") diff --git a/tests/import/pkgstar_all_array/__init__.py b/tests/import/pkgstar_all_array/__init__.py new file mode 100644 index 00000000000..03b012123fe --- /dev/null +++ b/tests/import/pkgstar_all_array/__init__.py @@ -0,0 +1,49 @@ +__all__ = ["publicFun", "PublicClass", "dynamicFun"] + + +# Definitions below should always be imported by a star import +def publicFun(): + return 1 + + +class PublicClass: + def __init__(self): + self._val = 1 + + +# If __all__ support is enabled, definitions below +# should not be imported by a star import +def unlistedFun(): + return 0 + + +class UnlistedClass: + def __init__(self): + self._val = 0 + + +# Definitions below should be not be imported by a star import +# (they start with an underscore, and are not listed in __all__) +def _privateFun(): + return -1 + + +class _PrivateClass: + def __init__(self): + self._val = -1 + + +# Test lazy loaded function, as used by extmod/asyncio: +# Works with a star import only if __all__ support is enabled +_attrs = { + "dynamicFun": "funcs", +} + + +def __getattr__(attr): + mod = _attrs.get(attr, None) + if mod is None: + raise AttributeError(attr) + value = getattr(__import__(mod, globals(), locals(), True, 1), attr) + globals()[attr] = value + return value diff --git a/tests/import/pkgstar_all_array/funcs.py b/tests/import/pkgstar_all_array/funcs.py new file mode 100644 index 00000000000..7540d70f66b --- /dev/null +++ b/tests/import/pkgstar_all_array/funcs.py @@ -0,0 +1,2 @@ +def dynamicFun(): + return 777 diff --git a/tests/import/pkgstar_all_inval/__init__.py b/tests/import/pkgstar_all_inval/__init__.py new file mode 100644 index 00000000000..7022476c19b --- /dev/null +++ b/tests/import/pkgstar_all_inval/__init__.py @@ -0,0 +1 @@ +__all__ = 42 diff --git a/tests/import/pkgstar_all_miss/__init__.py b/tests/import/pkgstar_all_miss/__init__.py new file mode 100644 index 00000000000..f9bbb538072 --- /dev/null +++ b/tests/import/pkgstar_all_miss/__init__.py @@ -0,0 +1,8 @@ +__all__ = ("existingFun", "missingFun") + + +def existingFun(): + return None + + +# missingFun is not defined, should raise an error on import diff --git a/tests/import/pkgstar_all_tuple/__init__.py b/tests/import/pkgstar_all_tuple/__init__.py new file mode 100644 index 00000000000..433ddc8e976 --- /dev/null +++ b/tests/import/pkgstar_all_tuple/__init__.py @@ -0,0 +1,22 @@ +__all__ = ("publicFun2", "PublicClass2") + + +# Definitions below should always be imported by a star import +def publicFun2(): + return 2 + + +class PublicClass2: + def __init__(self): + self._val = 2 + + +# If __all__ support is enabled, definitions below +# should not be imported by a star import +def unlistedFun2(): + return 0 + + +class UnlistedClass2: + def __init__(self): + self._val = 0 diff --git a/tests/import/pkgstar_default/__init__.py b/tests/import/pkgstar_default/__init__.py new file mode 100644 index 00000000000..4947e4ce7f1 --- /dev/null +++ b/tests/import/pkgstar_default/__init__.py @@ -0,0 +1,20 @@ +# When __all__ is undefined, star import should only +# show objects that do not start with an underscore + + +def visibleFun(): + return 42 + + +class VisibleClass: + def __init__(self): + self._val = 42 + + +def _hiddenFun(): + return -1 + + +class _HiddenClass: + def __init__(self): + self._val = -1 diff --git a/tests/inlineasm/asmbcc.py b/tests/inlineasm/asmbcc.py deleted file mode 100644 index 08967d48c74..00000000000 --- a/tests/inlineasm/asmbcc.py +++ /dev/null @@ -1,29 +0,0 @@ -# test bcc instructions -# at the moment only tests beq, narrow and wide versions - - -@micropython.asm_thumb -def f(r0): - mov(r1, r0) - - mov(r0, 10) - cmp(r1, 1) - beq(end) - - mov(r0, 20) - cmp(r1, 2) - beq_n(end) - - mov(r0, 30) - cmp(r1, 3) - beq_w(end) - - mov(r0, 0) - - label(end) - - -print(f(0)) -print(f(1)) -print(f(2)) -print(f(3)) diff --git a/tests/inlineasm/asmbcc.py.exp b/tests/inlineasm/asmbcc.py.exp deleted file mode 100644 index 39da7d1a99e..00000000000 --- a/tests/inlineasm/asmbcc.py.exp +++ /dev/null @@ -1,4 +0,0 @@ -0 -10 -20 -30 diff --git a/tests/inlineasm/rv32/asm_ext_zba.py b/tests/inlineasm/rv32/asm_ext_zba.py new file mode 100644 index 00000000000..75f3573c864 --- /dev/null +++ b/tests/inlineasm/rv32/asm_ext_zba.py @@ -0,0 +1,18 @@ +@micropython.asm_rv32 +def test_sh1add(a0, a1): + sh1add(a0, a0, a1) + + +@micropython.asm_rv32 +def test_sh2add(a0, a1): + sh2add(a0, a0, a1) + + +@micropython.asm_rv32 +def test_sh3add(a0, a1): + sh3add(a0, a0, a1) + + +print(hex(test_sh1add(10, 20))) +print(hex(test_sh2add(10, 20))) +print(hex(test_sh3add(10, 20))) diff --git a/tests/inlineasm/rv32/asm_ext_zba.py.exp b/tests/inlineasm/rv32/asm_ext_zba.py.exp new file mode 100644 index 00000000000..5f56bd95642 --- /dev/null +++ b/tests/inlineasm/rv32/asm_ext_zba.py.exp @@ -0,0 +1,3 @@ +0x28 +0x3c +0x64 diff --git a/tests/inlineasm/rv32/asmargs.py b/tests/inlineasm/rv32/asmargs.py new file mode 100644 index 00000000000..78afd511150 --- /dev/null +++ b/tests/inlineasm/rv32/asmargs.py @@ -0,0 +1,44 @@ +# test passing arguments + + +@micropython.asm_rv32 +def arg0(): + c_li(a0, 1) + + +print(arg0()) + + +@micropython.asm_rv32 +def arg1(a0): + addi(a0, a0, 1) + + +print(arg1(1)) + + +@micropython.asm_rv32 +def arg2(a0, a1): + add(a0, a0, a1) + + +print(arg2(1, 2)) + + +@micropython.asm_rv32 +def arg3(a0, a1, a2): + add(a0, a0, a1) + add(a0, a0, a2) + + +print(arg3(1, 2, 3)) + + +@micropython.asm_rv32 +def arg4(a0, a1, a2, a3): + add(a0, a0, a1) + add(a0, a0, a2) + add(a0, a0, a3) + + +print(arg4(1, 2, 3, 4)) diff --git a/tests/inlineasm/asmargs.py.exp b/tests/inlineasm/rv32/asmargs.py.exp similarity index 100% rename from tests/inlineasm/asmargs.py.exp rename to tests/inlineasm/rv32/asmargs.py.exp diff --git a/tests/inlineasm/rv32/asmarith.py b/tests/inlineasm/rv32/asmarith.py new file mode 100644 index 00000000000..8b864c0b3b2 --- /dev/null +++ b/tests/inlineasm/rv32/asmarith.py @@ -0,0 +1,79 @@ +# test arithmetic opcodes + + +@micropython.asm_rv32 +def f1(): + li(a0, 0x100) + li(a1, 1) + add(a0, a0, a1) + addi(a0, a0, 1) + addi(a0, a0, -2) + sub(a0, a0, a1) + c_add(a0, a1) + c_addi(a0, -1) + c_sub(a0, a1) + + +print(hex(f1())) + + +@micropython.asm_rv32 +def f2(): + li(a0, 0x10FF) + li(a1, 1) + and_(a2, a0, a1) + andi(a3, a0, 0x10) + or_(a2, a2, a3) + ori(a2, a2, 8) + li(a1, 0x200) + c_or(a2, a1) + li(a1, 0xF0) + mv(a0, a2) + c_and(a0, a1) + li(a1, 0x101) + xor(a0, a0, a1) + xori(a0, a0, 0x101) + c_xor(a0, a1) + + +print(hex(f2())) + + +@micropython.asm_rv32 +def f3(a0, a1): + slt(a0, a0, a1) + + +print(f3(0xFFFFFFF0, 0xFFFFFFF1)) +print(f3(0x0, 0xFFFFFFF1)) +print(f3(0xFFFFFFF1, 0xFFFFFFF1)) +print(f3(0xFFFFFFF1, 0xFFFFFFF0)) + + +@micropython.asm_rv32 +def f4(a0, a1): + sltu(a0, a0, a1) + + +print(f3(0xFFFFFFF0, 0xFFFFFFF1)) +print(f3(0x0, 0xFFFFFFF1)) +print(f3(0xFFFFFFF1, 0xFFFFFFF1)) +print(f3(0xFFFFFFF1, 0xFFFFFFF0)) + + +@micropython.asm_rv32 +def f5(a0): + slti(a0, a0, -2) + + +print(f5(-1)) +print(f5(-3)) + + +@micropython.asm_rv32 +def f6(a0): + sltiu(a0, a0, -2) + + +print(f6(-1)) +print(f6(-3)) diff --git a/tests/inlineasm/rv32/asmarith.py.exp b/tests/inlineasm/rv32/asmarith.py.exp new file mode 100644 index 00000000000..7da4dd5c93c --- /dev/null +++ b/tests/inlineasm/rv32/asmarith.py.exp @@ -0,0 +1,14 @@ +0xfe +0x111 +1 +0 +0 +0 +1 +0 +0 +0 +0 +1 +0 +1 diff --git a/tests/inlineasm/rv32/asmbranch.py b/tests/inlineasm/rv32/asmbranch.py new file mode 100644 index 00000000000..d7d059d4067 --- /dev/null +++ b/tests/inlineasm/rv32/asmbranch.py @@ -0,0 +1,161 @@ +# test branch instructions + + +@micropython.asm_rv32 +def tbeq(a0): + mv(a1, a0) + + li(a0, 10) + li(a2, 1) + beq(a1, a2, end) + + li(a0, 20) + li(a2, 2) + beq(a1, a2, end) + + li(a0, 30) + li(a2, 3) + beq(a1, a2, end) + + li(a0, 0) + + label(end) + + +print(tbeq(0)) +print(tbeq(1)) +print(tbeq(2)) +print(tbeq(3)) + + +@micropython.asm_rv32 +def tbne(a0): + mv(a1, a0) + + li(a0, 10) + li(a2, 1) + bne(a1, a2, end) + + li(a0, 20) + li(a2, 2) + bne(a1, a2, end) + + li(a0, 30) + li(a2, 3) + bne(a1, a2, end) + + li(a0, 0) + + label(end) + + +print(tbne(0)) +print(tbne(1)) +print(tbne(2)) +print(tbne(3)) + + +@micropython.asm_rv32 +def tbgeu(a0): + mv(a1, a0) + + li(a0, 1) + li(a2, 2) + bgeu(a1, a2, end) + li(a0, 0) + + label(end) + + +print(tbgeu(0)) +print(tbgeu(1)) +print(tbgeu(2)) +print(tbgeu(3)) + + +@micropython.asm_rv32 +def tbltu(a0): + mv(a1, a0) + + li(a0, 1) + li(a2, 2) + bltu(a1, a2, end) + li(a0, 0) + + label(end) + + +print(tbltu(0)) +print(tbltu(1)) +print(tbltu(2)) +print(tbltu(3)) + + +@micropython.asm_rv32 +def tbge(a0): + mv(a1, a0) + + li(a0, 1) + li(a2, -2) + bge(a1, a2, end) + li(a0, 0) + + label(end) + + +print(tbge(-3)) +print(tbge(-2)) +print(tbge(-1)) +print(tbge(0)) + + +@micropython.asm_rv32 +def tblt(a0): + mv(a1, a0) + + li(a0, 1) + li(a2, -2) + blt(a1, a2, end) + li(a0, 0) + + label(end) + + +print(tblt(-3)) +print(tblt(-2)) +print(tblt(-1)) +print(tblt(0)) + + +@micropython.asm_rv32 +def tcbeqz(a0): + mv(a1, a0) + + li(a0, 1) + c_beqz(a1, end) + li(a0, 0) + + label(end) + + +print(tcbeqz(0)) +print(tcbeqz(1)) +print(tcbeqz(2)) +print(tcbeqz(3)) + + +@micropython.asm_rv32 +def tcbnez(a0): + mv(a1, a0) + + li(a0, 1) + c_bnez(a1, end) + li(a0, 0) + + label(end) + + +print(tcbnez(0)) +print(tcbnez(1)) +print(tcbnez(2)) +print(tcbnez(3)) diff --git a/tests/basics/async_await.py.exp b/tests/inlineasm/rv32/asmbranch.py.exp similarity index 59% rename from tests/basics/async_await.py.exp rename to tests/inlineasm/rv32/asmbranch.py.exp index b51c388a933..baae6914953 100644 --- a/tests/basics/async_await.py.exp +++ b/tests/inlineasm/rv32/asmbranch.py.exp @@ -1,32 +1,32 @@ -4 -3 -2 -1 -0 0 -1 +10 +20 +30 +10 +20 +10 +10 0 0 -2 1 -0 -0 1 -0 -0 -3 -2 1 +1 +0 0 0 1 +1 +1 +1 +0 0 0 -2 1 0 0 -1 0 0 -finished +1 +1 +1 diff --git a/tests/inlineasm/rv32/asmconst.py b/tests/inlineasm/rv32/asmconst.py new file mode 100644 index 00000000000..2b6363a43db --- /dev/null +++ b/tests/inlineasm/rv32/asmconst.py @@ -0,0 +1,49 @@ +# test constants in assembler + + +@micropython.asm_rv32 +def c1(): + li(a0, 0xFFFFFFFF) + li(a1, 0xF0000000) + sub(a0, a0, a1) + + +print(hex(c1())) + + +@micropython.asm_rv32 +def c2(): + lui(a0, 0x12345) + li(a1, 0x678) + add(a0, a0, a1) + + +print(hex(c2())) + + +@micropython.asm_rv32 +def c3() -> uint: + lui(a0, 0) + addi(a0, a0, 0x7FF) + + +print(hex(c3())) + + +@micropython.asm_rv32 +def c4() -> uint: + lui(a0, 0) + addi(a0, a0, -1) + + +print(hex(c4())) + + +@micropython.asm_rv32 +def c5(): + c_lui(a0, 1) + c_li(a1, 1) + c_add(a0, a1) + + +print(hex(c5())) diff --git a/tests/inlineasm/rv32/asmconst.py.exp b/tests/inlineasm/rv32/asmconst.py.exp new file mode 100644 index 00000000000..0c713a84148 --- /dev/null +++ b/tests/inlineasm/rv32/asmconst.py.exp @@ -0,0 +1,5 @@ +0xfffffff +0x12345678 +0x7ff +0xffffffff +0x1001 diff --git a/tests/inlineasm/rv32/asmcsr.py b/tests/inlineasm/rv32/asmcsr.py new file mode 100644 index 00000000000..f27e2aa5e34 --- /dev/null +++ b/tests/inlineasm/rv32/asmcsr.py @@ -0,0 +1,65 @@ +# test csr instructions + +# CSR 0x340 is `mscratch`. This test suite is only safe to run on a system +# where it is known that there is no other code that can read from or write +# to that register. The qemu port is one such system, as the CSR is only +# accessed when a machine exception occurs, and at that point it doesn't matter +# anymore whether these tests are running or not. + + +@micropython.asm_rv32 +def csr(): + li(a0, 0) + csrrw(zero, zero, 0x340) # All zeroes + csrrs(a1, zero, 0x340) # Read zeroes + c_bnez(a1, end) + addi(a0, a0, 1) + li(a1, 0xA5A5A5A5) + li(a2, 0x5A5A5A5A) + csrrs(a2, a1, 0x340) # Read zeroes, set 0xA5A5A5A5 + c_bnez(a2, end) + addi(a0, a0, 1) + csrrs(a3, zero, 0x340) # Read 0xA5A5A5A5 + bne(a3, a1, end) + addi(a0, a0, 1) + li(a2, 0xF0F0F0F0) + csrrc(zero, a2, 0x340) # Clear upper half + csrrs(a3, zero, 0x340) # Read 0x05050505 + xori(a2, a2, -1) + and_(a2, a1, a2) + bne(a2, a3, end) + addi(a0, a0, 1) + label(end) + + +print(csr()) + + +@micropython.asm_rv32 +def csri(): + li(a0, 0) + csrrwi(zero, 0x340, 15) # Write 0xF + csrrs(a1, zero, 0x340) # Read 0xF + csrrsi(a2, 0x340, 0) # Read + bne(a1, a2, end) + addi(a0, a0, 1) + csrrci(a2, 0x340, 0) # Read + bne(a1, a2, end) + addi(a0, a0, 1) + li(a2, 15) + bne(a1, a2, end) + addi(a0, a0, 1) + csrrci(zero, 0x340, 1) # Clear bit 1 + csrrs(a1, zero, 0x340) # Read 0xE + li(a2, 14) + bne(a1, a2, end) + addi(a0, a0, 1) + csrrsi(zero, 0x340, 1) # Set bit 1 + csrrs(a1, zero, 0x340) # Read 0xF + li(a2, 15) + bne(a1, a2, end) + addi(a0, a0, 1) + label(end) + + +print(csri()) diff --git a/tests/inlineasm/rv32/asmcsr.py.exp b/tests/inlineasm/rv32/asmcsr.py.exp new file mode 100644 index 00000000000..61c83cba41c --- /dev/null +++ b/tests/inlineasm/rv32/asmcsr.py.exp @@ -0,0 +1,2 @@ +4 +5 diff --git a/tests/inlineasm/rv32/asmdata.py b/tests/inlineasm/rv32/asmdata.py new file mode 100644 index 00000000000..5e555ef4bf4 --- /dev/null +++ b/tests/inlineasm/rv32/asmdata.py @@ -0,0 +1,33 @@ +# test the "data" directive + + +@micropython.asm_rv32 +def ret_num(a0) -> uint: + slli(a0, a0, 2) + addi(a0, a0, 16) + auipc(a1, 0) + add(a1, a1, a0) + lw(a0, 0(a1)) + jal(zero, HERE) + data(4, 0x12345678, 0x20000000, 0x40000000, 0x7FFFFFFF + 1, (1 << 32) - 2) + label(HERE) + + +for i in range(5): + print(hex(ret_num(i))) + + +@micropython.asm_rv32 +def ret_num_la(a0) -> uint: + slli(a0, a0, 2) + la(a1, DATA) + add(a1, a1, a0) + lw(a0, 0(a1)) + jal(zero, HERE) + label(DATA) + data(4, 0x12345678, 0x20000000, 0x40000000, 0x7FFFFFFF + 1, (1 << 32) - 2) + label(HERE) + + +for i in range(5): + print(hex(ret_num_la(i))) diff --git a/tests/inlineasm/rv32/asmdata.py.exp b/tests/inlineasm/rv32/asmdata.py.exp new file mode 100644 index 00000000000..79e92bdfa5d --- /dev/null +++ b/tests/inlineasm/rv32/asmdata.py.exp @@ -0,0 +1,10 @@ +0x12345678 +0x20000000 +0x40000000 +0x80000000 +0xfffffffe +0x12345678 +0x20000000 +0x40000000 +0x80000000 +0xfffffffe diff --git a/tests/inlineasm/rv32/asmdivmul.py b/tests/inlineasm/rv32/asmdivmul.py new file mode 100644 index 00000000000..e1120c6f63c --- /dev/null +++ b/tests/inlineasm/rv32/asmdivmul.py @@ -0,0 +1,63 @@ +@micropython.asm_rv32 +def sdiv(a0, a1): + div(a0, a0, a1) + + +@micropython.asm_rv32 +def udiv(a0, a1): + divu(a0, a0, a1) + + +@micropython.asm_rv32 +def srem(a0, a1): + rem(a0, a0, a1) + + +@micropython.asm_rv32 +def urem(a0, a1): + remu(a0, a0, a1) + + +print(sdiv(1234, 3)) +print(sdiv(-1234, 3)) +print(sdiv(1234, -3)) +print(sdiv(-1234, -3)) + +print(udiv(1234, 3)) +print(udiv(0xFFFFFFFF, 0x7FFFFFFF)) +print(udiv(0xFFFFFFFF, 0xFFFFFFFF)) + +print(srem(1234, 3)) +print(srem(-1234, 3)) +print(srem(1234, -3)) +print(srem(-1234, -3)) + +print(urem(1234, 3)) +print(urem(0xFFFFFFFF, 0x7FFFFFFF)) +print(urem(0xFFFFFFFF, 0xFFFFFFFF)) + + +@micropython.asm_rv32 +def m1(a0, a1): + mul(a0, a0, a1) + + +@micropython.asm_rv32 +def m2(a0, a1): + mulh(a0, a0, a1) + + +@micropython.asm_rv32 +def m3(a0, a1): + mulhu(a0, a0, a1) + + +@micropython.asm_rv32 +def m4(a0, a1): + mulhsu(a0, a0, a1) + + +print(m1(0xFFFFFFFF, 2)) +print(m2(0xFFFFFFFF, 0xFFFFFFF0)) +print(m3(0xFFFFFFFF, 0xFFFFFFF0)) +print(m4(0xFFFFFFFF, 0xFFFFFFF0)) diff --git a/tests/inlineasm/rv32/asmdivmul.py.exp b/tests/inlineasm/rv32/asmdivmul.py.exp new file mode 100644 index 00000000000..60d28635f79 --- /dev/null +++ b/tests/inlineasm/rv32/asmdivmul.py.exp @@ -0,0 +1,18 @@ +411 +-411 +-411 +411 +411 +2 +1 +1 +-1 +1 +-1 +1 +1 +0 +-2 +0 +-17 +-1 diff --git a/tests/inlineasm/rv32/asmjump.py b/tests/inlineasm/rv32/asmjump.py new file mode 100644 index 00000000000..fe87d3f968b --- /dev/null +++ b/tests/inlineasm/rv32/asmjump.py @@ -0,0 +1,115 @@ +@micropython.asm_rv32 +def f1(): + li(a0, 0) + la(a1, END) + c_jr(a1) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + label(END) + + +print(f1()) + + +@micropython.asm_rv32 +def f2(): + addi(sp, sp, -4) + c_swsp(ra, 0) + li(ra, 0) + li(a0, 0) + c_jal(END) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + label(END) + bne(ra, zero, SUCCESS) + c_addi(a0, 2) + label(SUCCESS) + c_lwsp(ra, 0) + addi(sp, sp, 4) + + +print(f2()) + + +@micropython.asm_rv32 +def f3(): + li(a0, 0) + c_j(END) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + label(END) + + +print(f3()) + + +@micropython.asm_rv32 +def f4(): + addi(sp, sp, -4) + c_swsp(ra, 0) + li(ra, 0) + li(a0, 0) + la(a1, END) + c_jalr(a1) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + label(END) + bne(ra, zero, SUCCESS) + c_addi(a0, 2) + label(SUCCESS) + c_lwsp(ra, 0) + addi(sp, sp, 4) + + +print(f4()) + + +@micropython.asm_rv32 +def f5(): + li(a0, 0) + li(a1, 0) + jal(a1, END) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + label(END) + bne(a1, zero, SUCCESS) + c_addi(a0, 2) + label(SUCCESS) + + +print(f5()) + + +@micropython.asm_rv32 +def f6(): + li(a0, 0) + la(a1, JUMP) + li(a2, 0) + jalr(a2, a1, 10) + label(JUMP) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + c_addi(a0, 1) + bne(a2, zero, SUCCESS) + c_addi(a0, 2) + label(SUCCESS) + + +print(f6()) diff --git a/tests/inlineasm/rv32/asmjump.py.exp b/tests/inlineasm/rv32/asmjump.py.exp new file mode 100644 index 00000000000..f7eb44d66e0 --- /dev/null +++ b/tests/inlineasm/rv32/asmjump.py.exp @@ -0,0 +1,6 @@ +0 +0 +0 +0 +0 +0 diff --git a/tests/inlineasm/rv32/asmloadstore.py b/tests/inlineasm/rv32/asmloadstore.py new file mode 100644 index 00000000000..2c49e07b41a --- /dev/null +++ b/tests/inlineasm/rv32/asmloadstore.py @@ -0,0 +1,86 @@ +# test load/store opcodes + + +@micropython.asm_rv32 +def l(): + li(a5, 4) + addi(sp, sp, -12) + li(a0, 0x123) + c_swsp(a0, 0) + addi(a1, a0, 0x111) + c_swsp(a1, 4) + addi(a2, a1, 0x111) + c_swsp(a2, 8) + mv(a4, sp) + c_lw(a3, 0(a4)) + bne(a3, a0, END) + addi(a5, a5, -1) + lw(a3, 4(a4)) + bne(a3, a1, END) + addi(a5, a5, -1) + lhu(a3, 8(a4)) + bne(a3, a2, END) + addi(a5, a5, -1) + lbu(a0, 8(a4)) + addi(a0, a0, 0x300) + bne(a0, a2, END) + addi(a5, a5, -1) + label(END) + addi(sp, sp, 12) + mv(a0, a5) + + +print(l()) + + +@micropython.asm_rv32 +def s(): + li(a5, 4) + addi(sp, sp, -12) + c_swsp(zero, 0) + c_swsp(zero, 4) + c_swsp(zero, 8) + li(a0, 0x12345) + mv(a4, sp) + c_sw(a0, 0(a4)) + sh(a0, 4(a4)) + sb(a0, 8(a4)) + li(a1, 0xFFFF) + and_(a1, a0, a1) + andi(a2, a0, 0xFF) + lw(a3, 0(sp)) + bne(a3, a0, END) + addi(a5, a5, -1) + lw(a3, 4(sp)) + bne(a3, a1, END) + addi(a5, a5, -1) + lw(a3, 8(sp)) + bne(a3, a2, END) + addi(a5, a5, -1) + label(END) + addi(sp, sp, 12) + mv(a0, a5) + + +print(s()) + + +@micropython.asm_rv32 +def lu(): + li(a5, 4) + addi(sp, sp, -8) + li(a0, 0xF1234567) + c_swsp(a0, 0) + c_swsp(a0, 4) + lh(a1, 0(sp)) + blt(a1, zero, END) + addi(a5, a5, -1) + lb(a2, 4(sp)) + blt(a2, zero, END) + addi(a5, a5, -1) + label(END) + addi(sp, sp, 8) + mv(a0, a5) + + +print(lu()) diff --git a/tests/inlineasm/rv32/asmloadstore.py.exp b/tests/inlineasm/rv32/asmloadstore.py.exp new file mode 100644 index 00000000000..4539bbf2d22 --- /dev/null +++ b/tests/inlineasm/rv32/asmloadstore.py.exp @@ -0,0 +1,3 @@ +0 +1 +2 diff --git a/tests/inlineasm/rv32/asmrettype.py b/tests/inlineasm/rv32/asmrettype.py new file mode 100644 index 00000000000..fc7ae61d152 --- /dev/null +++ b/tests/inlineasm/rv32/asmrettype.py @@ -0,0 +1,33 @@ +# test return type of inline asm + + +@micropython.asm_rv32 +def ret_obj(a0) -> object: + pass + + +ret_obj(print)(1) + + +@micropython.asm_rv32 +def ret_bool(a0) -> bool: + pass + + +print(ret_bool(0), ret_bool(1)) + + +@micropython.asm_rv32 +def ret_int(a0) -> int: + slli(a0, a0, 29) + + +print(ret_int(0), hex(ret_int(1)), hex(ret_int(2)), hex(ret_int(4))) + + +@micropython.asm_rv32 +def ret_uint(a0) -> uint: + slli(a0, a0, 29) + + +print(ret_uint(0), hex(ret_uint(1)), hex(ret_uint(2)), hex(ret_uint(4))) diff --git a/tests/inlineasm/asmrettype.py.exp b/tests/inlineasm/rv32/asmrettype.py.exp similarity index 100% rename from tests/inlineasm/asmrettype.py.exp rename to tests/inlineasm/rv32/asmrettype.py.exp diff --git a/tests/inlineasm/rv32/asmsanity.py b/tests/inlineasm/rv32/asmsanity.py new file mode 100644 index 00000000000..1a16d3504db --- /dev/null +++ b/tests/inlineasm/rv32/asmsanity.py @@ -0,0 +1,204 @@ +TEMPLATE3 = """ +@micropython.asm_rv32 +def f(): + {}({}, {}, {}) +""" + +TEMPLATE2 = """ +@micropython.asm_rv32 +def f(): + {}({}, {}) +""" + +TEMPLATE1 = """ +@micropython.asm_rv32 +def f(): + {}({}) +""" + + +REGISTERS = [ + "zero", + "s0", + "s1", + "s2", + "s3", + "s4", + "s5", + "s6", + "s7", + "s8", + "s9", + "s10", + "s11", + "a0", + "a1", + "a2", + "a3", + "a4", + "a5", + "a6", + "a7", + "tp", + "gp", + "sp", + "ra", + "t0", + "t1", + "t2", + "t3", + "t4", + "t5", + "t6", + "x0", + "x1", + "x2", + "x3", + "x4", + "x5", + "x6", + "x7", + "x8", + "x9", + "x10", + "x11", + "x12", + "x13", + "x14", + "x15", + "x16", + "x17", + "x18", + "x19", + "x20", + "x21", + "x22", + "x23", + "x24", + "x25", + "x26", + "x27", + "x28", + "x29", + "x30", + "x31", +] + + +def harness(opcode, fragment, tag): + try: + exec(fragment) + except SyntaxError: + print(tag, opcode) + + +for opcode in ("slli", "srli", "srai"): + harness(opcode, TEMPLATE3.format(opcode, "a0", "a0", -1), "-") + harness(opcode, TEMPLATE3.format(opcode, "a0", "a0", 33), "+") + +for opcode in ("c_slli", "c_srli", "c_srai"): + harness(opcode, TEMPLATE2.format(opcode, "a0", -1), "-") + harness(opcode, TEMPLATE2.format(opcode, "a0", 33), "+") + +harness("c_slli", TEMPLATE2.format("c_slli", "zero", 0), "0") +harness("c_slli", TEMPLATE2.format("c_slli", "x0", 0), "0") + +for opcode in ("c_srli", "c_srai"): + for register in REGISTERS: + harness(opcode, TEMPLATE2.format(opcode, register, 0), register) + +for opcode in ("c_mv", "c_add"): + harness(opcode, TEMPLATE2.format(opcode, "a0", "zero"), "0l") + harness(opcode, TEMPLATE2.format(opcode, "zero", "a0"), "0r") + harness(opcode, TEMPLATE2.format(opcode, "zero", "zero"), "0b") + +harness("c_jr", TEMPLATE1.format("c_jr", "zero"), "0") + +for opcode in ("addi", "andi", "ori", "slti", "sltiu", "xori"): + harness(opcode, TEMPLATE3.format(opcode, "a0", "a0", 0x7FF), ">=s") + harness(opcode, TEMPLATE3.format(opcode, "a0", "a0", 0x800), ">s") + harness(opcode, TEMPLATE3.format(opcode, "a0", "a0", -2048), "<=s") + harness(opcode, TEMPLATE3.format(opcode, "a0", "a0", -2049), "=s") + harness(opcode, TEMPLATE.format(opcode, 0x800), ">s") + harness(opcode, TEMPLATE.format(opcode, -2048), "<=s") + harness(opcode, TEMPLATE.format(opcode, -2049), "0") +harness("c_addi", TEMPLATE2.format("c_andi", "zero", -512), "<0") +harness("c_addi", TEMPLATE2.format("c_andi", "s0", 0), "s0") +harness("c_addi", TEMPLATE2.format("c_andi", "s0", -100), "s") + +harness("c_andi", TEMPLATE2.format("c_andi", "zero", 0), "00") +harness("c_andi", TEMPLATE2.format("c_andi", "zero", 512), ">0") +harness("c_andi", TEMPLATE2.format("c_andi", "zero", -512), "<0") +harness("c_andi", TEMPLATE2.format("c_andi", "s0", 0), "s0") +harness("c_andi", TEMPLATE2.format("c_andi", "s0", -100), "s") + +C_REGISTERS = ( + "a0", + "a1", + "a2", + "a3", + "a4", + "a5", + "s0", + "s1", + "x8", + "x9", + "x10", + "x11", + "x12", + "x13", + "x14", + "x15", +) + +for opcode in ("c_and", "c_or", "c_xor"): + for source in REGISTERS: + for destination in REGISTERS: + if source in C_REGISTERS and destination in C_REGISTERS: + try: + exec( + """ +@micropython.asm_rv32 +def f(): + {}({}, {}) +""".format(opcode, source, destination) + ) + except SyntaxError: + print(source, destination, opcode) + else: + try: + exec( + """ +@micropython.asm_rv32 +def f(): + {}({}, {}) +""".format(opcode, source, destination) + ) + print(source, destination, opcode) + except SyntaxError: + pass + print(opcode) + +for opcode in ("c_lw", "c_sw"): + TEMPLATE = """ +@micropython.asm_rv32 +def f(): + {}(a0, {}(a0)) +""" + harness(opcode, TEMPLATE.format(opcode, 60), ">=s") + harness(opcode, TEMPLATE.format(opcode, 61), ">s") + harness(opcode, TEMPLATE.format(opcode, -60), "<=s") + harness(opcode, TEMPLATE.format(opcode, -61), "s addi +s andi +s ori +s slti +s sltiu +s xori +s lb +s lbu +s lh +s lhu +s lw +s sb +s sh +s sw +0 c_addi +<0 c_addi +s c_addi +00 c_andi +>0 c_andi +<0 c_andi +s c_andi +c_and +c_or +c_xor +>s c_lw +s c_sw + int: + addmi(a2, a2, {}) +print(f1(0)) +""" + +for value in (-32768, -32767, 32512, 32513, 0): + try: + exec(ADDMI_TEMPLATE.format(value)) + except SyntaxError as error: + print(error) + + +@micropython.asm_xtensa +def a2(a2, a3) -> int: + addx2(a2, a2, a3) + + +@micropython.asm_xtensa +def a4(a2, a3) -> int: + addx4(a2, a2, a3) + + +@micropython.asm_xtensa +def a8(a2, a3) -> int: + addx8(a2, a2, a3) + + +@micropython.asm_xtensa +def s2(a2, a3) -> int: + subx2(a2, a2, a3) + + +@micropython.asm_xtensa +def s4(a2, a3) -> int: + subx4(a2, a2, a3) + + +@micropython.asm_xtensa +def s8(a2, a3) -> int: + subx8(a2, a2, a3) + + +for first, second in ((100, 100), (-100, 100), (-100, -100), (100, -100)): + print("a2", a2(first, second)) + print("a4", a4(first, second)) + print("a8", a8(first, second)) + print("s2", s2(first, second)) + print("s4", s4(first, second)) + print("s8", s8(first, second)) + + +@micropython.asm_xtensa +def f5(a2) -> int: + neg(a2, a2) + + +for value in (0, -100, 100): + print(f5(value)) + + +@micropython.asm_xtensa +def f6(): + movi(a2, 0x100) + movi(a3, 1) + add(a2, a2, a3) + addi(a2, a2, 1) + addi(a2, a2, -2) + sub(a2, a2, a3) + + +print(hex(f6())) + + +@micropython.asm_xtensa +def f7(): + movi(a2, 0x10FF) + movi(a3, 1) + and_(a4, a2, a3) + or_(a4, a4, a3) + movi(a3, 0x200) + xor(a2, a4, a3) + + +print(hex(f7())) + + +@micropython.asm_xtensa +def f8(a2, a3): + add_n(a2, a2, a3) + + +print(f8(100, 200)) + + +@micropython.asm_xtensa +def f9(a2): + addi_n(a2, a2, 1) + + +print(f9(100)) + + +@micropython.asm_xtensa +def f10(a2, a3) -> uint: + mull(a2, a2, a3) + + +print(hex(f10(0xC0000000, 2))) diff --git a/tests/inlineasm/xtensa/asmarith.py.exp b/tests/inlineasm/xtensa/asmarith.py.exp new file mode 100644 index 00000000000..7aba46a27d9 --- /dev/null +++ b/tests/inlineasm/xtensa/asmarith.py.exp @@ -0,0 +1,40 @@ +10 +10 +0 +-32768 +-32767 is not a multiple of 256 +32512 +'addmi' integer 32513 isn't within range -32768..32512 +0 +a2 300 +a4 500 +a8 900 +s2 100 +s4 300 +s8 700 +a2 -100 +a4 -300 +a8 -700 +s2 -300 +s4 -500 +s8 -900 +a2 -300 +a4 -500 +a8 -900 +s2 -100 +s4 -300 +s8 -700 +a2 100 +a4 300 +a8 700 +s2 300 +s4 500 +s8 900 +0 +100 +-100 +0xff +0x201 +300 +101 +0x80000000 diff --git a/tests/inlineasm/xtensa/asmbranch.py b/tests/inlineasm/xtensa/asmbranch.py new file mode 100644 index 00000000000..22bcd5a7c71 --- /dev/null +++ b/tests/inlineasm/xtensa/asmbranch.py @@ -0,0 +1,299 @@ +# test branch instructions + + +@micropython.asm_xtensa +def tball(a2, a3) -> int: + mov(a4, a2) + movi(a2, 0) + ball(a4, a3, end) + movi(a2, -1) + label(end) + + +print(tball(0xFFFFFFFF, 0xFFFFFFFF)) +print(tball(0xFFFEFFFF, 0xFFFFFFFF)) +print(tball(0x00000000, 0xFFFFFFFF)) +print(tball(0xFFFFFFFF, 0x01010101)) + + +@micropython.asm_xtensa +def tbany(a2, a3) -> int: + mov(a4, a2) + movi(a2, 0) + bany(a4, a3, end) + movi(a2, -1) + label(end) + + +print(tbany(0xFFFFFFFF, 0xFFFFFFFF)) +print(tbany(0xFFFEFFFF, 0xFFFFFFFF)) +print(tbany(0x00000000, 0xFFFFFFFF)) +print(tbany(0xFFFFFFFF, 0x01010101)) + + +@micropython.asm_xtensa +def tbbc(a2, a3) -> int: + mov(a4, a2) + movi(a2, 0) + bbc(a4, a3, end) + movi(a2, -1) + label(end) + + +print(tbbc(0xFFFFFFFF, 4)) +print(tbbc(0xFFFEFFFF, 16)) +print(tbbc(0x00000000, 1)) + + +BBCI_TEMPLATE = """ +@micropython.asm_xtensa +def tbbci(a2) -> int: + mov(a3, a2) + movi(a2, 0) + bbci(a3, {}, end) + movi(a2, -1) + label(end) + +print(tbbci({})) +""" + + +for value, bit in ((0xFFFFFFFF, 4), (0xFFFEFFFF, 16), (0x00000000, 1)): + try: + exec(BBCI_TEMPLATE.format(bit, value)) + except SyntaxError as error: + print(error) + + +@micropython.asm_xtensa +def tbbs(a2, a3) -> int: + mov(a4, a2) + movi(a2, 0) + bbs(a4, a3, end) + movi(a2, -1) + label(end) + + +print(tbbs(0x00000000, 4)) +print(tbbs(0x00010000, 16)) +print(tbbs(0xFFFFFFFF, 1)) + + +BBSI_TEMPLATE = """ +@micropython.asm_xtensa +def tbbsi(a2) -> int: + mov(a3, a2) + movi(a2, 0) + bbsi(a3, {}, end) + movi(a2, -1) + label(end) + +print(tbbsi({})) +""" + + +for value, bit in ((0x00000000, 4), (0x00010000, 16), (0xFFFFFFFF, 1)): + try: + exec(BBSI_TEMPLATE.format(bit, value)) + except SyntaxError as error: + print(error) + + +@micropython.asm_xtensa +def tbeq(a2, a3) -> int: + mov(a4, a2) + movi(a2, 0) + beq(a4, a3, end) + movi(a2, -1) + label(end) + + +print(tbeq(0x00000000, 0x00000000)) +print(tbeq(0x00010000, 0x00000000)) +print(tbeq(0xFFFFFFFF, 0xFFFFFFFF)) + + +@micropython.asm_xtensa +def tbeqz(a2) -> int: + mov(a3, a2) + movi(a2, 0) + beqz(a3, end) + movi(a2, -1) + label(end) + + +print(tbeqz(0)) +print(tbeqz(0x12345678)) +print(tbeqz(-1)) + + +@micropython.asm_xtensa +def tbge(a2, a3) -> int: + mov(a4, a2) + movi(a2, 0) + bge(a4, a3, end) + movi(a2, -1) + label(end) + + +print(tbge(0x00000000, 0x00000000)) +print(tbge(0x00010000, 0x00000000)) +print(tbge(0xF0000000, 0xFFFFFFFF)) + + +@micropython.asm_xtensa +def tbgeu(a2, a3) -> int: + mov(a4, a2) + movi(a2, 0) + bgeu(a4, a3, end) + movi(a2, -1) + label(end) + + +print(tbgeu(0x00000000, 0x00000000)) +print(tbgeu(0x00010000, 0x00000000)) +print(tbgeu(0xF0000000, 0xFFFFFFFF)) +print(tbgeu(0xFFFFFFFF, 0xF0000000)) + + +@micropython.asm_xtensa +def tbgez(a2) -> int: + mov(a3, a2) + movi(a2, 0) + bgez(a3, end) + movi(a2, -1) + label(end) + + +print(tbgez(0)) +print(tbgez(0x12345678)) +print(tbgez(-1)) + + +@micropython.asm_xtensa +def tblt(a2, a3) -> int: + mov(a4, a2) + movi(a2, 0) + blt(a4, a3, end) + movi(a2, -1) + label(end) + + +print(tblt(0x00000000, 0x00000000)) +print(tblt(0x00010000, 0x00000000)) +print(tblt(0xF0000000, 0xFFFFFFFF)) + + +@micropython.asm_xtensa +def tbltu(a2, a3) -> int: + mov(a4, a2) + movi(a2, 0) + bltu(a4, a3, end) + movi(a2, -1) + label(end) + + +print(tbltu(0x00000000, 0x00000000)) +print(tbltu(0x00010000, 0x00000000)) +print(tbltu(0xF0000000, 0xFFFFFFFF)) +print(tbltu(0xFFFFFFFF, 0xF0000000)) + + +@micropython.asm_xtensa +def tbltz(a2) -> int: + mov(a3, a2) + movi(a2, 0) + bltz(a3, end) + movi(a2, -1) + label(end) + + +print(tbltz(0)) +print(tbltz(0x12345678)) +print(tbltz(-1)) + + +@micropython.asm_xtensa +def tbnall(a2, a3) -> int: + mov(a4, a2) + movi(a2, 0) + bnall(a4, a3, end) + movi(a2, -1) + label(end) + + +print(tbnall(0xFFFFFFFF, 0xFFFFFFFF)) +print(tbnall(0xFFFEFFFF, 0xFFFFFFFF)) +print(tbnall(0x00000000, 0xFFFFFFFF)) +print(tbnall(0xFFFFFFFF, 0x01010101)) + + +@micropython.asm_xtensa +def tbne(a2, a3) -> int: + mov(a4, a2) + movi(a2, 0) + bne(a4, a3, end) + movi(a2, -1) + label(end) + + +print(tbne(0x00000000, 0x00000000)) +print(tbne(0x00010000, 0x00000000)) +print(tbne(0xFFFFFFFF, 0xFFFFFFFF)) + + +@micropython.asm_xtensa +def tbnez(a2) -> int: + mov(a3, a2) + movi(a2, 0) + bnez(a3, end) + movi(a2, -1) + label(end) + + +print(tbnez(0)) +print(tbnez(0x12345678)) +print(tbnez(-1)) + + +@micropython.asm_xtensa +def tbnone(a2, a3) -> int: + mov(a4, a2) + movi(a2, 0) + bnone(a4, a3, end) + movi(a2, -1) + label(end) + + +print(tbnone(0xFFFFFFFF, 0xFFFFFFFF)) +print(tbnone(0xFFFEFFFF, 0xFFFFFFFF)) +print(tbnone(0x00000000, 0xFFFFFFFF)) +print(tbnone(0x10101010, 0x01010101)) + + +@micropython.asm_xtensa +def tbeqz_n(a2) -> int: + mov(a3, a2) + movi(a2, 0) + beqz_n(a3, end) + movi(a2, -1) + label(end) + + +print(tbeqz_n(0)) +print(tbeqz_n(0x12345678)) +print(tbeqz_n(-1)) + + +@micropython.asm_xtensa +def tbnez_n(a2) -> int: + mov(a3, a2) + movi(a2, 0) + bnez(a3, end) + movi(a2, -1) + label(end) + + +print(tbnez_n(0)) +print(tbnez_n(0x12345678)) +print(tbnez_n(-1)) diff --git a/tests/inlineasm/xtensa/asmbranch.py.exp b/tests/inlineasm/xtensa/asmbranch.py.exp new file mode 100644 index 00000000000..319a4b435ef --- /dev/null +++ b/tests/inlineasm/xtensa/asmbranch.py.exp @@ -0,0 +1,66 @@ +0 +-1 +-1 +0 +0 +0 +-1 +0 +-1 +0 +0 +-1 +0 +0 +-1 +0 +0 +-1 +0 +0 +0 +-1 +0 +0 +-1 +-1 +0 +0 +-1 +0 +0 +-1 +0 +0 +0 +-1 +-1 +-1 +0 +-1 +-1 +0 +-1 +-1 +-1 +0 +-1 +0 +0 +-1 +-1 +0 +-1 +-1 +0 +0 +-1 +-1 +0 +0 +0 +-1 +-1 +-1 +0 +0 diff --git a/tests/inlineasm/xtensa/asmjump.py b/tests/inlineasm/xtensa/asmjump.py new file mode 100644 index 00000000000..f41c9482319 --- /dev/null +++ b/tests/inlineasm/xtensa/asmjump.py @@ -0,0 +1,26 @@ +@micropython.asm_xtensa +def jump() -> int: + movi(a2, 0) + j(NEXT) + addi(a2, a2, 1) + j(DONE) + label(NEXT) + addi(a2, a2, 2) + label(DONE) + + +print(jump()) + + +@micropython.asm_xtensa +def jumpx() -> int: + call0(ENTRY) + label(ENTRY) + movi(a2, 0) + addi(a3, a0, 12) + jx(a3) + movi(a2, 1) + movi(a2, 2) + + +print(jumpx()) diff --git a/tests/inlineasm/xtensa/asmjump.py.exp b/tests/inlineasm/xtensa/asmjump.py.exp new file mode 100644 index 00000000000..51993f072d5 --- /dev/null +++ b/tests/inlineasm/xtensa/asmjump.py.exp @@ -0,0 +1,2 @@ +2 +2 diff --git a/tests/inlineasm/xtensa/asmloadstore.py b/tests/inlineasm/xtensa/asmloadstore.py new file mode 100644 index 00000000000..85f1f8a561b --- /dev/null +++ b/tests/inlineasm/xtensa/asmloadstore.py @@ -0,0 +1,101 @@ +import array + +# On the ESP8266 the generated code gets put into the IRAM segment, which is +# only word-addressable. Therefore, to test byte and halfword load/store +# opcodes some memory must be reserved in the DRAM segment. This also happens +# to work on the ESP32 too. + +BYTE_DATA = array.array("B", (0x11, 0x22, 0x33, 0x44)) +WORD_DATA = array.array("h", (100, 200, -100, -200)) +DWORD_DATA = array.array("i", (100_000, -200_000, 300_000, -400_000)) + + +@micropython.asm_xtensa +def tl32r() -> int: + nop() + j(CODE) + align(4) + label(DATA) + data(1, 1, 2, 3, 4, 5, 6, 7) + align(4) + label(CODE) + nop_n() + nop_n() + l32r(a2, DATA) + + +print(hex(tl32r())) + + +@micropython.asm_xtensa +def tl32i() -> uint: + call0(ENTRY) + align(4) + label(ENTRY) + l32i(a2, a0, 0) + nop() + + +print(hex(tl32i())) + + +@micropython.asm_xtensa +def tl8ui(a2) -> uint: + mov(a3, a2) + l8ui(a2, a3, 1) + + +print(hex(tl8ui(BYTE_DATA))) + + +@micropython.asm_xtensa +def tl16ui(a2) -> uint: + mov(a3, a2) + l16ui(a2, a3, 2) + + +print(tl16ui(WORD_DATA)) + + +@micropython.asm_xtensa +def tl16si(a2) -> int: + mov(a3, a2) + l16si(a2, a3, 6) + + +print(tl16si(WORD_DATA)) + + +@micropython.asm_xtensa +def ts8i(a2, a3): + s8i(a3, a2, 1) + + +ts8i(BYTE_DATA, 0xFF) +print(BYTE_DATA) + + +@micropython.asm_xtensa +def ts16i(a2, a3): + s16i(a3, a2, 2) + + +ts16i(WORD_DATA, -123) +print(WORD_DATA) + + +@micropython.asm_xtensa +def ts32i(a2, a3) -> uint: + s32i(a3, a2, 4) + + +ts32i(DWORD_DATA, -123456) +print(DWORD_DATA) + + +@micropython.asm_xtensa +def tl32i_n(a2) -> uint: + l32i_n(a2, a2, 8) + + +print(tl32i_n(DWORD_DATA)) diff --git a/tests/inlineasm/xtensa/asmloadstore.py.exp b/tests/inlineasm/xtensa/asmloadstore.py.exp new file mode 100644 index 00000000000..ec453a1cad6 --- /dev/null +++ b/tests/inlineasm/xtensa/asmloadstore.py.exp @@ -0,0 +1,9 @@ +0x4030201 +0xf0002022 +0x22 +200 +-200 +array('B', [17, 255, 51, 68]) +array('h', [100, -123, -100, -200]) +array('i', [100000, -123456, 300000, -400000]) +300000 diff --git a/tests/inlineasm/xtensa/asmmisc.py b/tests/inlineasm/xtensa/asmmisc.py new file mode 100644 index 00000000000..271ab836625 --- /dev/null +++ b/tests/inlineasm/xtensa/asmmisc.py @@ -0,0 +1,25 @@ +@micropython.asm_xtensa +def tnop(a2, a3, a4, a5): + nop() + + +out2 = tnop(0x100, 0x200, 0x300, 0x400) +print(out2 == 0x100) + + +@micropython.asm_xtensa +def tnop_n(a2, a3, a4, a5): + nop_n() + + +out2 = tnop_n(0x100, 0x200, 0x300, 0x400) +print(out2 == 0x100) + + +@micropython.asm_xtensa +def tmov_n(a2, a3): + mov_n(a4, a3) + add(a2, a4, a3) + + +print(tmov_n(0, 1)) diff --git a/tests/inlineasm/xtensa/asmmisc.py.exp b/tests/inlineasm/xtensa/asmmisc.py.exp new file mode 100644 index 00000000000..eefaa35daf0 --- /dev/null +++ b/tests/inlineasm/xtensa/asmmisc.py.exp @@ -0,0 +1,3 @@ +True +True +2 diff --git a/tests/inlineasm/xtensa/asmshift.py b/tests/inlineasm/xtensa/asmshift.py new file mode 100644 index 00000000000..271ca1ccd49 --- /dev/null +++ b/tests/inlineasm/xtensa/asmshift.py @@ -0,0 +1,137 @@ +@micropython.asm_xtensa +def lsl1(a2): + slli(a2, a2, 1) + + +print(hex(lsl1(0x123))) + + +@micropython.asm_xtensa +def lsl23(a2): + slli(a2, a2, 23) + + +print(hex(lsl23(1))) + + +@micropython.asm_xtensa +def lsr1(a2): + srli(a2, a2, 1) + + +print(hex(lsr1(0x123))) + + +@micropython.asm_xtensa +def lsr15(a2): + srli(a2, a2, 15) + + +print(hex(lsr15(0x80000000))) + + +@micropython.asm_xtensa +def asr1(a2): + srai(a2, a2, 1) + + +print(hex(asr1(0x123))) + + +@micropython.asm_xtensa +def asr31(a2): + srai(a2, a2, 31) + + +print(hex(asr31(0x80000000))) + + +@micropython.asm_xtensa +def lsl1r(a2): + movi(a3, 1) + ssl(a3) + sll(a2, a2) + + +print(hex(lsl1r(0x123))) + + +@micropython.asm_xtensa +def lsr1r(a2): + movi(a3, 1) + ssr(a3) + srl(a2, a2) + + +print(hex(lsr1r(0x123))) + + +@micropython.asm_xtensa +def asr1r(a2): + movi(a3, 1) + ssr(a3) + sra(a2, a2) + + +print(hex(asr1r(0x123))) + + +@micropython.asm_xtensa +def sll9(a2): + ssai(9) + sll(a2, a2) + + +print(hex(sll9(1))) + + +@micropython.asm_xtensa +def srlr(a2, a3): + ssa8l(a3) + srl(a2, a2) + + +print(hex(srlr(0x12340000, 2))) + + +@micropython.asm_xtensa +def sllr(a2, a3): + ssa8b(a3) + sll(a2, a2) + + +print(hex(sllr(0x1234, 2))) + + +@micropython.asm_xtensa +def srcr(a2, a3, a4): + ssr(a4) + src(a2, a2, a3) + + +print(hex(srcr(0x00000001, 0x80000000, 2))) + + +@micropython.asm_xtensa +def srai24(a2): + srai(a2, a2, 24) + + +print(hex(srai24(0x12345678))) + + +@micropython.asm_xtensa +def nsar(a2, a3): + nsa(a2, a3) + + +print(nsar(0x12345678, 0)) +print(nsar(0x12345678, -1)) + + +@micropython.asm_xtensa +def nsaur(a2, a3): + nsau(a2, a3) + + +print(nsaur(0x12345678, 0)) diff --git a/tests/inlineasm/xtensa/asmshift.py.exp b/tests/inlineasm/xtensa/asmshift.py.exp new file mode 100644 index 00000000000..3e2bb3b4aef --- /dev/null +++ b/tests/inlineasm/xtensa/asmshift.py.exp @@ -0,0 +1,17 @@ +0x246 +0x800000 +0x91 +0x10000 +0x91 +-0x1 +0x246 +0x91 +0x91 +0x800000 +0x1234 +0x12340000 +0x60000000 +0x12 +31 +31 +32 diff --git a/tests/internal_bench/class_create-0-empty.py b/tests/internal_bench/class_create-0-empty.py new file mode 100644 index 00000000000..1fd8ccd9257 --- /dev/null +++ b/tests/internal_bench/class_create-0-empty.py @@ -0,0 +1,11 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-1-slots.py b/tests/internal_bench/class_create-1-slots.py new file mode 100644 index 00000000000..9b3e4b9570d --- /dev/null +++ b/tests/internal_bench/class_create-1-slots.py @@ -0,0 +1,12 @@ +import bench + + +def test(num): + l = ["x"] + for i in range(num // 40): + + class X: + __slots__ = l + + +bench.run(test) diff --git a/tests/internal_bench/class_create-1.1-slots5.py b/tests/internal_bench/class_create-1.1-slots5.py new file mode 100644 index 00000000000..ccac77dec9d --- /dev/null +++ b/tests/internal_bench/class_create-1.1-slots5.py @@ -0,0 +1,12 @@ +import bench + + +def test(num): + l = ["a", "b", "c", "d", "x"] + for i in range(num // 40): + + class X: + __slots__ = l + + +bench.run(test) diff --git a/tests/internal_bench/class_create-2-classattr.py b/tests/internal_bench/class_create-2-classattr.py new file mode 100644 index 00000000000..049a7dab170 --- /dev/null +++ b/tests/internal_bench/class_create-2-classattr.py @@ -0,0 +1,11 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + x = 1 + + +bench.run(test) diff --git a/tests/internal_bench/class_create-2.1-classattr5.py b/tests/internal_bench/class_create-2.1-classattr5.py new file mode 100644 index 00000000000..5051e7dcca7 --- /dev/null +++ b/tests/internal_bench/class_create-2.1-classattr5.py @@ -0,0 +1,15 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + a = 0 + b = 0 + c = 0 + d = 0 + x = 1 + + +bench.run(test) diff --git a/tests/internal_bench/class_create-2.3-classattr5objs.py b/tests/internal_bench/class_create-2.3-classattr5objs.py new file mode 100644 index 00000000000..74540865dcd --- /dev/null +++ b/tests/internal_bench/class_create-2.3-classattr5objs.py @@ -0,0 +1,20 @@ +import bench + + +class Class: + pass + + +def test(num): + instance = Class() + for i in range(num // 40): + + class X: + a = instance + b = instance + c = instance + d = instance + x = instance + + +bench.run(test) diff --git a/tests/internal_bench/class_create-3-instancemethod.py b/tests/internal_bench/class_create-3-instancemethod.py new file mode 100644 index 00000000000..e8c201cb2c3 --- /dev/null +++ b/tests/internal_bench/class_create-3-instancemethod.py @@ -0,0 +1,12 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + def x(self): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-4-classmethod.py b/tests/internal_bench/class_create-4-classmethod.py new file mode 100644 index 00000000000..f34962bc671 --- /dev/null +++ b/tests/internal_bench/class_create-4-classmethod.py @@ -0,0 +1,13 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + @classmethod + def x(cls): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-4.1-classmethod_implicit.py b/tests/internal_bench/class_create-4.1-classmethod_implicit.py new file mode 100644 index 00000000000..f2d1fcfd188 --- /dev/null +++ b/tests/internal_bench/class_create-4.1-classmethod_implicit.py @@ -0,0 +1,12 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + def __new__(cls): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-5-staticmethod.py b/tests/internal_bench/class_create-5-staticmethod.py new file mode 100644 index 00000000000..06335566675 --- /dev/null +++ b/tests/internal_bench/class_create-5-staticmethod.py @@ -0,0 +1,13 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + @staticmethod + def x(): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-6-getattribute.py b/tests/internal_bench/class_create-6-getattribute.py new file mode 100644 index 00000000000..10a4fe7ce8d --- /dev/null +++ b/tests/internal_bench/class_create-6-getattribute.py @@ -0,0 +1,12 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + def __getattribute__(self, name): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-6.1-getattr.py b/tests/internal_bench/class_create-6.1-getattr.py new file mode 100644 index 00000000000..b4b9ba2f552 --- /dev/null +++ b/tests/internal_bench/class_create-6.1-getattr.py @@ -0,0 +1,12 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + def __getattr__(self, name): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-6.2-property.py b/tests/internal_bench/class_create-6.2-property.py new file mode 100644 index 00000000000..cf847b6dc9c --- /dev/null +++ b/tests/internal_bench/class_create-6.2-property.py @@ -0,0 +1,13 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + @property + def x(self): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-6.3-descriptor.py b/tests/internal_bench/class_create-6.3-descriptor.py new file mode 100644 index 00000000000..7b0a6357263 --- /dev/null +++ b/tests/internal_bench/class_create-6.3-descriptor.py @@ -0,0 +1,17 @@ +import bench + + +class D: + def __get__(self, instance, owner=None): + pass + + +def test(num): + descriptor = D() + for i in range(num // 40): + + class X: + x = descriptor + + +bench.run(test) diff --git a/tests/internal_bench/class_create-7-inherit.py b/tests/internal_bench/class_create-7-inherit.py new file mode 100644 index 00000000000..f48fb215e0a --- /dev/null +++ b/tests/internal_bench/class_create-7-inherit.py @@ -0,0 +1,14 @@ +import bench + + +def test(num): + class B: + pass + + for i in range(num // 40): + + class X(B): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-7.1-inherit_initsubclass.py b/tests/internal_bench/class_create-7.1-inherit_initsubclass.py new file mode 100644 index 00000000000..0660fa86258 --- /dev/null +++ b/tests/internal_bench/class_create-7.1-inherit_initsubclass.py @@ -0,0 +1,16 @@ +import bench + + +def test(num): + class B: + @classmethod + def __init_subclass__(cls): + pass + + for i in range(num // 40): + + class X(B): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-8-metaclass_setname.py b/tests/internal_bench/class_create-8-metaclass_setname.py new file mode 100644 index 00000000000..e4515b54279 --- /dev/null +++ b/tests/internal_bench/class_create-8-metaclass_setname.py @@ -0,0 +1,17 @@ +import bench + + +class D: + def __set_name__(self, owner, name): + pass + + +def test(num): + descriptor = D() + for i in range(num // 40): + + class X: + x = descriptor + + +bench.run(test) diff --git a/tests/internal_bench/class_create-8.1-metaclass_setname5.py b/tests/internal_bench/class_create-8.1-metaclass_setname5.py new file mode 100644 index 00000000000..5daa3f8471b --- /dev/null +++ b/tests/internal_bench/class_create-8.1-metaclass_setname5.py @@ -0,0 +1,21 @@ +import bench + + +class D: + def __set_name__(self, owner, name): + pass + + +def test(num): + descriptor = D() + for i in range(num // 40): + + class X: + a = descriptor + b = descriptor + c = descriptor + d = descriptor + x = descriptor + + +bench.run(test) diff --git a/tests/internal_bench/class_instance-0-object.py b/tests/internal_bench/class_instance-0-object.py new file mode 100644 index 00000000000..401c8ea7e3c --- /dev/null +++ b/tests/internal_bench/class_instance-0-object.py @@ -0,0 +1,11 @@ +import bench + +X = object + + +def test(num): + for i in range(num // 5): + x = X() + + +bench.run(test) diff --git a/tests/internal_bench/class_instance-0.1-object-gc.py b/tests/internal_bench/class_instance-0.1-object-gc.py new file mode 100644 index 00000000000..7c475963a8e --- /dev/null +++ b/tests/internal_bench/class_instance-0.1-object-gc.py @@ -0,0 +1,13 @@ +import bench +import gc + +X = object + + +def test(num): + for i in range(num // 5): + x = X() + gc.collect() + + +bench.run(test) diff --git a/tests/internal_bench/class_instance-1-empty.py b/tests/internal_bench/class_instance-1-empty.py new file mode 100644 index 00000000000..617d47a86e9 --- /dev/null +++ b/tests/internal_bench/class_instance-1-empty.py @@ -0,0 +1,13 @@ +import bench + + +class X: + pass + + +def test(num): + for i in range(num // 5): + x = X() + + +bench.run(test) diff --git a/tests/internal_bench/class_instance-1.1-classattr.py b/tests/internal_bench/class_instance-1.1-classattr.py new file mode 100644 index 00000000000..4e667533d4a --- /dev/null +++ b/tests/internal_bench/class_instance-1.1-classattr.py @@ -0,0 +1,13 @@ +import bench + + +class X: + x = 0 + + +def test(num): + for i in range(num // 5): + x = X() + + +bench.run(test) diff --git a/tests/internal_bench/class_instance-1.2-func.py b/tests/internal_bench/class_instance-1.2-func.py new file mode 100644 index 00000000000..21bf7a1ac48 --- /dev/null +++ b/tests/internal_bench/class_instance-1.2-func.py @@ -0,0 +1,14 @@ +import bench + + +class X: + def f(self): + pass + + +def test(num): + for i in range(num // 5): + x = X() + + +bench.run(test) diff --git a/tests/internal_bench/class_instance-1.3-empty-gc.py b/tests/internal_bench/class_instance-1.3-empty-gc.py new file mode 100644 index 00000000000..a5108ef8e81 --- /dev/null +++ b/tests/internal_bench/class_instance-1.3-empty-gc.py @@ -0,0 +1,15 @@ +import bench +import gc + + +class X: + pass + + +def test(num): + for i in range(num // 5): + x = X() + gc.collect() + + +bench.run(test) diff --git a/tests/internal_bench/class_instance-2-init.py b/tests/internal_bench/class_instance-2-init.py new file mode 100644 index 00000000000..86619d31548 --- /dev/null +++ b/tests/internal_bench/class_instance-2-init.py @@ -0,0 +1,14 @@ +import bench + + +class X: + def __init__(self): + pass + + +def test(num): + for i in range(num // 5): + x = X() + + +bench.run(test) diff --git a/tests/internal_bench/class_instance-2.1-init_super.py b/tests/internal_bench/class_instance-2.1-init_super.py new file mode 100644 index 00000000000..38bca5fef87 --- /dev/null +++ b/tests/internal_bench/class_instance-2.1-init_super.py @@ -0,0 +1,14 @@ +import bench + + +class X: + def __init__(self): + return super().__init__() + + +def test(num): + for i in range(num // 5): + x = X() + + +bench.run(test) diff --git a/tests/internal_bench/class_instance-2.2-new.py b/tests/internal_bench/class_instance-2.2-new.py new file mode 100644 index 00000000000..dc5e78ea5ef --- /dev/null +++ b/tests/internal_bench/class_instance-2.2-new.py @@ -0,0 +1,14 @@ +import bench + + +class X: + def __new__(cls): + return super().__new__(cls) + + +def test(num): + for i in range(num // 5): + x = X() + + +bench.run(test) diff --git a/tests/internal_bench/class_instance-3-del.py b/tests/internal_bench/class_instance-3-del.py new file mode 100644 index 00000000000..af700f72a94 --- /dev/null +++ b/tests/internal_bench/class_instance-3-del.py @@ -0,0 +1,14 @@ +import bench + + +class X: + def __del__(self): + pass + + +def test(num): + for i in range(num // 5): + x = X() + + +bench.run(test) diff --git a/tests/internal_bench/class_instance-3.1-del-gc.py b/tests/internal_bench/class_instance-3.1-del-gc.py new file mode 100644 index 00000000000..311c71c3571 --- /dev/null +++ b/tests/internal_bench/class_instance-3.1-del-gc.py @@ -0,0 +1,16 @@ +import bench +import gc + + +class X: + def __del__(self): + pass + + +def test(num): + for i in range(num // 5): + x = X() + gc.collect() + + +bench.run(test) diff --git a/tests/internal_bench/class_instance-4-slots.py b/tests/internal_bench/class_instance-4-slots.py new file mode 100644 index 00000000000..51b067fedf0 --- /dev/null +++ b/tests/internal_bench/class_instance-4-slots.py @@ -0,0 +1,13 @@ +import bench + + +class X: + __slots__ = ["x"] + + +def test(num): + for i in range(num // 5): + x = X() + + +bench.run(test) diff --git a/tests/internal_bench/class_instance-4.1-slots5.py b/tests/internal_bench/class_instance-4.1-slots5.py new file mode 100644 index 00000000000..8f5c2ecb456 --- /dev/null +++ b/tests/internal_bench/class_instance-4.1-slots5.py @@ -0,0 +1,13 @@ +import bench + + +class X: + __slots__ = ["a", "b", "c", "d", "x"] + + +def test(num): + for i in range(num // 5): + x = X() + + +bench.run(test) diff --git a/tests/internal_bench/var-6.2-instance-speciallookup.py b/tests/internal_bench/var-6.2-instance-speciallookup.py new file mode 100644 index 00000000000..fee12b2f930 --- /dev/null +++ b/tests/internal_bench/var-6.2-instance-speciallookup.py @@ -0,0 +1,19 @@ +import bench + + +class Foo: + def __init__(self): + self.num = 20000000 + + def __delattr__(self, name): # just trigger the 'special lookups' flag on the class + pass + + +def test(num): + o = Foo() + i = 0 + while i < o.num: + i += 1 + + +bench.run(test) diff --git a/tests/internal_bench/var-6.3-instance-property.py b/tests/internal_bench/var-6.3-instance-property.py new file mode 100644 index 00000000000..b4426ef7928 --- /dev/null +++ b/tests/internal_bench/var-6.3-instance-property.py @@ -0,0 +1,17 @@ +import bench + + +class Foo: + @property + def num(self): + return 20000000 + + +def test(num): + o = Foo() + i = 0 + while i < o.num: + i += 1 + + +bench.run(test) diff --git a/tests/internal_bench/var-6.4-instance-descriptor.py b/tests/internal_bench/var-6.4-instance-descriptor.py new file mode 100644 index 00000000000..b4df69f878f --- /dev/null +++ b/tests/internal_bench/var-6.4-instance-descriptor.py @@ -0,0 +1,20 @@ +import bench + + +class Descriptor: + def __get__(self, instance, owner=None): + return 20000000 + + +class Foo: + num = Descriptor() + + +def test(num): + o = Foo() + i = 0 + while i < o.num: + i += 1 + + +bench.run(test) diff --git a/tests/internal_bench/var-6.5-instance-getattr.py b/tests/internal_bench/var-6.5-instance-getattr.py new file mode 100644 index 00000000000..3b2ef672110 --- /dev/null +++ b/tests/internal_bench/var-6.5-instance-getattr.py @@ -0,0 +1,16 @@ +import bench + + +class Foo: + def __getattr__(self, name): + return 20000000 + + +def test(num): + o = Foo() + i = 0 + while i < o.num: + i += 1 + + +bench.run(test) diff --git a/tests/internal_bench/var-6.6-instance-builtin_ordered.py b/tests/internal_bench/var-6.6-instance-builtin_ordered.py new file mode 100644 index 00000000000..02d75252301 --- /dev/null +++ b/tests/internal_bench/var-6.6-instance-builtin_ordered.py @@ -0,0 +1,12 @@ +import bench + + +def test(num): + i = 0 + o = set() # object with largest rom-frozen ordered locals_dict + n = "__contains__" # last element in that dict for longest lookup + while i < num: + i += hasattr(o, n) # True, converts to 1 + + +bench.run(test) diff --git a/tests/internal_bench/var-9-getattr.py b/tests/internal_bench/var-9-getattr.py new file mode 100644 index 00000000000..69d2bfed2e0 --- /dev/null +++ b/tests/internal_bench/var-9-getattr.py @@ -0,0 +1,16 @@ +import bench + + +class Foo: + pass + + +def test(num): + o = Foo() + o.num = num + i = 0 + while i < getattr(o, "num", num): + i += 1 + + +bench.run(test) diff --git a/tests/internal_bench/var-9.1-getattr_default.py b/tests/internal_bench/var-9.1-getattr_default.py new file mode 100644 index 00000000000..e803d39b326 --- /dev/null +++ b/tests/internal_bench/var-9.1-getattr_default.py @@ -0,0 +1,15 @@ +import bench + + +class Foo: + pass + + +def test(num): + o = Foo() + i = 0 + while i < getattr(o, "num", num): + i += 1 + + +bench.run(test) diff --git a/tests/internal_bench/var-9.2-getattr_default_special.py b/tests/internal_bench/var-9.2-getattr_default_special.py new file mode 100644 index 00000000000..c48ec0742cf --- /dev/null +++ b/tests/internal_bench/var-9.2-getattr_default_special.py @@ -0,0 +1,16 @@ +import bench + + +class Foo: + def __delattr__(self, name): # just trigger the 'special lookups' flag on the class + pass + + +def test(num): + o = Foo() + i = 0 + while i < getattr(o, "num", num): + i += 1 + + +bench.run(test) diff --git a/tests/internal_bench/var-9.3-except_ok.py b/tests/internal_bench/var-9.3-except_ok.py new file mode 100644 index 00000000000..efc1a8f858c --- /dev/null +++ b/tests/internal_bench/var-9.3-except_ok.py @@ -0,0 +1,23 @@ +import bench + + +class Foo: + pass + + +def test(num): + o = Foo() + o.num = num + + def get(): + try: + return o.num + except AttributeError: + return num + + i = 0 + while i < get(): + i += 1 + + +bench.run(test) diff --git a/tests/internal_bench/var-9.4-except_selfinduced.py b/tests/internal_bench/var-9.4-except_selfinduced.py new file mode 100644 index 00000000000..544609ca4b6 --- /dev/null +++ b/tests/internal_bench/var-9.4-except_selfinduced.py @@ -0,0 +1,22 @@ +import bench + + +class Foo: + pass + + +def test(num): + o = Foo() + + def get(): + try: + raise AttributeError + except AttributeError: + return num + + i = 0 + while i < get(): + i += 1 + + +bench.run(test) diff --git a/tests/internal_bench/var-9.5-except_error.py b/tests/internal_bench/var-9.5-except_error.py new file mode 100644 index 00000000000..caf83fa46ab --- /dev/null +++ b/tests/internal_bench/var-9.5-except_error.py @@ -0,0 +1,22 @@ +import bench + + +class Foo: + pass + + +def test(num): + o = Foo() + + def get(): + try: + return o.num + except AttributeError: + return num + + i = 0 + while i < get(): + i += 1 + + +bench.run(test) diff --git a/tests/internal_bench/var-9.6-except_error_special.py b/tests/internal_bench/var-9.6-except_error_special.py new file mode 100644 index 00000000000..8bc395b4d7e --- /dev/null +++ b/tests/internal_bench/var-9.6-except_error_special.py @@ -0,0 +1,23 @@ +import bench + + +class Foo: + def __delattr__(self, name): # just trigger the 'special lookups' flag on the class + pass + + +def test(num): + o = Foo() + + def get(): + try: + return o.num + except AttributeError: + return num + + i = 0 + while i < get(): + i += 1 + + +bench.run(test) diff --git a/tests/io/builtin_print_file.py b/tests/io/builtin_print_file.py index 822356a6cc3..e00f58635a9 100644 --- a/tests/io/builtin_print_file.py +++ b/tests/io/builtin_print_file.py @@ -13,5 +13,5 @@ try: print(file=1) -except (AttributeError, OSError): # CPython and uPy differ in error message +except (AttributeError, OSError): # CPython and MicroPython differ in error message print("Error") diff --git a/tests/io/file_seek.py b/tests/io/file_seek.py index 3990df84090..b9f9593786f 100644 --- a/tests/io/file_seek.py +++ b/tests/io/file_seek.py @@ -30,5 +30,5 @@ try: f.seek(1) except (OSError, ValueError): - # CPy raises ValueError, uPy raises OSError + # CPy raises ValueError, MPy raises OSError print("OSError or ValueError") diff --git a/tests/micropython/builtin_execfile.py b/tests/micropython/builtin_execfile.py index a905521c662..4fd4d66d4ee 100644 --- a/tests/micropython/builtin_execfile.py +++ b/tests/micropython/builtin_execfile.py @@ -16,7 +16,9 @@ def __init__(self, data): self.off = 0 def ioctl(self, request, arg): - return 0 + if request == 4: # MP_STREAM_CLOSE + return 0 + return -1 def readinto(self, buf): buf[:] = memoryview(self.data)[self.off : self.off + len(buf)] @@ -73,3 +75,24 @@ def open(self, file, mode): # Unmount the VFS object. vfs.umount(fs) + + +class EvilFilesystem: + def mount(self, readonly, mkfs): + print("mount", readonly, mkfs) + + def umount(self): + print("umount") + + def open(self, file, mode): + return None + + +fs = EvilFilesystem() +vfs.mount(fs, "/test_mnt") +try: + execfile("/test_mnt/test.py") + print("ExecFile succeeded") +except OSError: + print("OSError") +vfs.umount(fs) diff --git a/tests/micropython/builtin_execfile.py.exp b/tests/micropython/builtin_execfile.py.exp index 49703d57076..d93dee547b6 100644 --- a/tests/micropython/builtin_execfile.py.exp +++ b/tests/micropython/builtin_execfile.py.exp @@ -5,3 +5,6 @@ open /test.py rb 123 TypeError umount +mount False False +OSError +umount diff --git a/tests/micropython/builtin_tstring.py b/tests/micropython/builtin_tstring.py new file mode 100644 index 00000000000..8de0acfd98b --- /dev/null +++ b/tests/micropython/builtin_tstring.py @@ -0,0 +1,24 @@ +# This tests the built-in __template__ (which is MicroPython specific). + +# Test a varying number of arguments. +print(__template__(())) +print(__template__((), ())) +print(__template__((), None, None)) +print(__template__((), None, None, None)) +print(__template__((), None, None, None, None)) +print(__template__((), None, None, None, None, None)) + +# Test two strings and one interpolation. +print(__template__(("Hello ", "!"), (42, "x", None, ""))) + +# Test not enough arguments. +try: + print(__template__()) +except TypeError as er: + print(repr(er)) + +# Test two arguments with second not being a tuple/list. +try: + print(__template__((), None)) +except TypeError as er: + print(repr(er)) diff --git a/tests/micropython/builtin_tstring.py.exp b/tests/micropython/builtin_tstring.py.exp new file mode 100644 index 00000000000..b6b47306124 --- /dev/null +++ b/tests/micropython/builtin_tstring.py.exp @@ -0,0 +1,9 @@ +Template(strings=(), interpolations=()) +Template(strings=(), interpolations=()) +Template(strings=(), interpolations=()) +Template(strings=(), interpolations=()) +Template(strings=(), interpolations=(Interpolation(None, None, None, None),)) +Template(strings=(), interpolations=(Interpolation(None, None, None, None),)) +Template(strings=('Hello ', '!'), interpolations=(Interpolation(42, 'x', None, ''),)) +TypeError('function missing 1 required positional arguments',) +TypeError("object 'NoneType' isn't a tuple or list",) diff --git a/tests/micropython/const_annotated.py b/tests/micropython/const_annotated.py new file mode 100644 index 00000000000..4d01389e43a --- /dev/null +++ b/tests/micropython/const_annotated.py @@ -0,0 +1,18 @@ +# Test type annotations in combination with const. +# This test will only work when MICROPY_COMP_CONST and MICROPY_COMP_CONST_TUPLE are enabled. + +from micropython import const + +_X0: bool = const(True) +_X1: int = const(123) +_X2: str = const("test") +_X3: tuple = const((1, 2)) +_X4: tuple[bool, int] = const((True, 4)) +_X5: bytes = b"\x01\x02\x03" + +print(_X0) +print(_X1) +print(_X2) +print(_X3) +print(_X4) +print(_X5) diff --git a/tests/micropython/const_annotated.py.exp b/tests/micropython/const_annotated.py.exp new file mode 100644 index 00000000000..65edb58e72e --- /dev/null +++ b/tests/micropython/const_annotated.py.exp @@ -0,0 +1,6 @@ +True +123 +test +(1, 2) +(True, 4) +b'\x01\x02\x03' diff --git a/tests/micropython/const_error.py b/tests/micropython/const_error.py index d35be530a7c..950360e4dc7 100644 --- a/tests/micropython/const_error.py +++ b/tests/micropython/const_error.py @@ -18,8 +18,6 @@ def test_syntax(code): # these operations are not supported within const test_syntax("A = const(1 @ 2)") -test_syntax("A = const(1 / 2)") -test_syntax("A = const(1 ** -2)") test_syntax("A = const(1 << -2)") test_syntax("A = const(1 >> -2)") test_syntax("A = const(1 % 0)") diff --git a/tests/micropython/const_error.py.exp b/tests/micropython/const_error.py.exp index 3edc3efe9c3..bef69eb32ea 100644 --- a/tests/micropython/const_error.py.exp +++ b/tests/micropython/const_error.py.exp @@ -5,5 +5,3 @@ SyntaxError SyntaxError SyntaxError SyntaxError -SyntaxError -SyntaxError diff --git a/tests/micropython/const_float.py b/tests/micropython/const_float.py new file mode 100644 index 00000000000..c3a0df0276b --- /dev/null +++ b/tests/micropython/const_float.py @@ -0,0 +1,23 @@ +# test constant optimisation, with consts that are floats + +from micropython import const + +# check we can make consts from floats +F1 = const(2.5) +F2 = const(-0.3) +print(type(F1), F1) +print(type(F2), F2) + +# check arithmetic with floats +F3 = const(F1 + F2) +F4 = const(F1**2) +print(F3, F4) + +# check int operations with float results +F5 = const(1 / 2) +F6 = const(2**-2) +print(F5, F6) + +# note: we also test float expression folding when +# we're compiling test cases in tests/float, as +# many expressions are resolved at compile time. diff --git a/tests/micropython/const_float.py.exp b/tests/micropython/const_float.py.exp new file mode 100644 index 00000000000..17a86a6d936 --- /dev/null +++ b/tests/micropython/const_float.py.exp @@ -0,0 +1,4 @@ + 2.5 + -0.3 +2.2 6.25 +0.5 0.25 diff --git a/tests/micropython/const_math.py b/tests/micropython/const_math.py new file mode 100644 index 00000000000..7ee5edc6d32 --- /dev/null +++ b/tests/micropython/const_math.py @@ -0,0 +1,18 @@ +# Test expressions based on math module constants +try: + import math +except ImportError: + print("SKIP") + raise SystemExit + +from micropython import const + +# check that we can make consts from math constants +# (skip if the target has MICROPY_COMP_MODULE_CONST disabled) +try: + exec("two_pi = const(2.0 * math.pi)") +except SyntaxError: + print("SKIP") + raise SystemExit + +print(math.cos(two_pi)) diff --git a/tests/micropython/const_math.py.exp b/tests/micropython/const_math.py.exp new file mode 100644 index 00000000000..d3827e75a5c --- /dev/null +++ b/tests/micropython/const_math.py.exp @@ -0,0 +1 @@ +1.0 diff --git a/tests/micropython/decorator_error.py b/tests/micropython/decorator_error.py index 94772ac1e5f..b5eae65b292 100644 --- a/tests/micropython/decorator_error.py +++ b/tests/micropython/decorator_error.py @@ -1,4 +1,4 @@ -# test syntax errors for uPy-specific decorators +# test syntax errors for MicroPython-specific decorators def test_syntax(code): diff --git a/tests/micropython/emg_exc.py b/tests/micropython/emg_exc.py index 9a09956c861..42b709375ce 100644 --- a/tests/micropython/emg_exc.py +++ b/tests/micropython/emg_exc.py @@ -1,10 +1,9 @@ # test that emergency exceptions work -import micropython import sys try: - import io + import io, micropython except ImportError: print("SKIP") raise SystemExit diff --git a/tests/micropython/emg_exc.py.exp b/tests/micropython/emg_exc.py.exp index 0d4b80ce2bc..d18012d691a 100644 --- a/tests/micropython/emg_exc.py.exp +++ b/tests/micropython/emg_exc.py.exp @@ -1,4 +1,4 @@ Traceback (most recent call last): -, line 22, in f +, line 21, in f ValueError: 1 diff --git a/tests/micropython/emg_exc.py.native.exp b/tests/micropython/emg_exc.py.native.exp new file mode 100644 index 00000000000..9677c526a9c --- /dev/null +++ b/tests/micropython/emg_exc.py.native.exp @@ -0,0 +1,2 @@ +ValueError: 1 + diff --git a/tests/micropython/extreme_exc.py b/tests/micropython/extreme_exc.py index ad819e408fd..17323b566d3 100644 --- a/tests/micropython/extreme_exc.py +++ b/tests/micropython/extreme_exc.py @@ -1,6 +1,10 @@ # test some extreme cases of allocating exceptions and tracebacks -import micropython +try: + import micropython +except ImportError: + print("SKIP") + raise SystemExit # Check for stackless build, which can't call functions without # allocating a frame on the heap. diff --git a/tests/micropython/heap_lock.py b/tests/micropython/heap_lock.py index f2892a6dc58..209a3143461 100644 --- a/tests/micropython/heap_lock.py +++ b/tests/micropython/heap_lock.py @@ -1,6 +1,10 @@ # check that heap_lock/heap_unlock work as expected -import micropython +try: + import micropython +except ImportError: + print("SKIP") + raise SystemExit l = [] l2 = list(range(100)) diff --git a/tests/micropython/heap_locked.py b/tests/micropython/heap_locked.py index d9e5b5d4090..d9d99493dd6 100644 --- a/tests/micropython/heap_locked.py +++ b/tests/micropython/heap_locked.py @@ -1,8 +1,10 @@ # test micropython.heap_locked() -import micropython +try: + import micropython -if not hasattr(micropython, "heap_locked"): + micropython.heap_locked +except (AttributeError, ImportError): print("SKIP") raise SystemExit diff --git a/tests/micropython/heapalloc.py b/tests/micropython/heapalloc.py index e19f8d0255d..010bf878a07 100644 --- a/tests/micropython/heapalloc.py +++ b/tests/micropython/heapalloc.py @@ -1,6 +1,10 @@ # check that we can do certain things without allocating heap memory -import micropython +try: + import micropython +except ImportError: + print("SKIP") + raise SystemExit # Check for stackless build, which can't call functions without # allocating a frame on heap. diff --git a/tests/micropython/heapalloc_exc_compressed.py b/tests/micropython/heapalloc_exc_compressed.py index aa071d641c2..96fe3ca4f7e 100644 --- a/tests/micropython/heapalloc_exc_compressed.py +++ b/tests/micropython/heapalloc_exc_compressed.py @@ -1,4 +1,10 @@ -import micropython +try: + import micropython + + micropython.heap_lock +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit # Tests both code paths for built-in exception raising. # mp_obj_new_exception_msg_varg (exception requires decompression at raise-time to format) diff --git a/tests/micropython/heapalloc_exc_compressed_emg_exc.py b/tests/micropython/heapalloc_exc_compressed_emg_exc.py index 48ce9dd69e6..31d937b8f93 100644 --- a/tests/micropython/heapalloc_exc_compressed_emg_exc.py +++ b/tests/micropython/heapalloc_exc_compressed_emg_exc.py @@ -1,4 +1,10 @@ -import micropython +try: + import micropython + + micropython.heap_lock +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit # Does the full test from heapalloc_exc_compressed.py but while the heap is # locked (this can only work when the emergency exception buf is enabled). diff --git a/tests/micropython/heapalloc_exc_raise.py b/tests/micropython/heapalloc_exc_raise.py index 99810e00750..917ccf42f1e 100644 --- a/tests/micropython/heapalloc_exc_raise.py +++ b/tests/micropython/heapalloc_exc_raise.py @@ -1,6 +1,12 @@ # Test that we can raise and catch (preallocated) exception # without memory allocation. -import micropython +try: + import micropython + + micropython.heap_lock +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit e = ValueError("error") diff --git a/tests/micropython/heapalloc_fail_bytearray.py b/tests/micropython/heapalloc_fail_bytearray.py index 1bf7ddd600b..9ee73d1c583 100644 --- a/tests/micropython/heapalloc_fail_bytearray.py +++ b/tests/micropython/heapalloc_fail_bytearray.py @@ -1,6 +1,12 @@ # test handling of failed heap allocation with bytearray -import micropython +try: + import micropython + + micropython.heap_lock +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit class GetSlice: diff --git a/tests/micropython/heapalloc_fail_dict.py b/tests/micropython/heapalloc_fail_dict.py index ce2d158bd09..04c79183578 100644 --- a/tests/micropython/heapalloc_fail_dict.py +++ b/tests/micropython/heapalloc_fail_dict.py @@ -1,6 +1,12 @@ # test handling of failed heap allocation with dict -import micropython +try: + import micropython + + micropython.heap_lock +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit # create dict x = 1 diff --git a/tests/micropython/heapalloc_fail_list.py b/tests/micropython/heapalloc_fail_list.py index 9a2e9a555f3..7afb6dc1019 100644 --- a/tests/micropython/heapalloc_fail_list.py +++ b/tests/micropython/heapalloc_fail_list.py @@ -1,6 +1,12 @@ # test handling of failed heap allocation with list -import micropython +try: + import micropython + + micropython.heap_lock +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit class GetSlice: diff --git a/tests/micropython/heapalloc_fail_memoryview.py b/tests/micropython/heapalloc_fail_memoryview.py index da2d1abffa6..17e3e126247 100644 --- a/tests/micropython/heapalloc_fail_memoryview.py +++ b/tests/micropython/heapalloc_fail_memoryview.py @@ -1,6 +1,12 @@ # test handling of failed heap allocation with memoryview -import micropython +try: + import micropython + + micropython.heap_lock +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit class GetSlice: diff --git a/tests/micropython/heapalloc_fail_set.py b/tests/micropython/heapalloc_fail_set.py index 3c347660ad7..0c4d85eef62 100644 --- a/tests/micropython/heapalloc_fail_set.py +++ b/tests/micropython/heapalloc_fail_set.py @@ -1,6 +1,12 @@ # test handling of failed heap allocation with set -import micropython +try: + import micropython + + micropython.heap_lock +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit # create set x = 1 diff --git a/tests/micropython/heapalloc_fail_tstring.py b/tests/micropython/heapalloc_fail_tstring.py new file mode 100644 index 00000000000..94687d57bfb --- /dev/null +++ b/tests/micropython/heapalloc_fail_tstring.py @@ -0,0 +1,475 @@ +# Test template string (t-string) operations with heap allocation failure + +import micropython +from string.templatelib import Template, Interpolation + +i1 = Interpolation(1, "1") +i2 = Interpolation(2, "2") + +micropython.heap_lock() +try: + args = [] + for i in range(9): + args.append("x" * 100) + args.append(Interpolation(i, "x" + str(i))) + args.append("x" * 100) + Template(*args) + print("FAIL: Template creation") +except MemoryError: + print("OK: Template creation") +micropython.heap_unlock() + +# Multiple string concatenation +micropython.heap_lock() +try: + Template("first", "second", "third", "fourth") + print("FAIL: Multi string concat") +except MemoryError: + print("OK: Multi string concat") +micropython.heap_unlock() + +# Mixed constructor +micropython.heap_lock() +try: + Template("a", i1, "b", i2, "c", "d", "e") + print("FAIL: Mixed constructor") +except MemoryError: + print("OK: Mixed constructor") +micropython.heap_unlock() + +# Template.__str__() +t = t"Hello {42} world {99}" +micropython.heap_lock() +try: + str(t) + print("FAIL: Template.__str__()") +except MemoryError: + print("OK: Template.__str__()") +micropython.heap_unlock() + +# Template.values property +vals = list(range(10)) +t_many = t"{vals[0]}{vals[1]}{vals[2]}{vals[3]}{vals[4]}" +micropython.heap_lock() +try: + t_many.values + print("FAIL: Template.values") +except MemoryError: + print("OK: Template.values") +micropython.heap_unlock() + +n = 20 +args_large = [] +for i in range(n): + args_large.append("") + args_large.append(Interpolation(i, "x" + str(i))) +args_large.append("") +t_large = Template(*args_large) +micropython.heap_lock() +try: + t_large.values + print("FAIL: Large values array") +except MemoryError: + print("OK: Large values array") +micropython.heap_unlock() + +# Template concatenation +t1 = t"Hello" +t2 = t"World" +micropython.heap_lock() +try: + t1 + t2 + print("FAIL: Template concatenation") +except MemoryError: + print("OK: Template concatenation") +micropython.heap_unlock() + +# Template iterator +t_iter = t"a{1}b{2}c" +micropython.heap_lock() +try: + iter(t_iter) + print("FAIL: Template iterator") +except MemoryError: + print("OK: Template iterator") +micropython.heap_unlock() + +# Iterator next +t_iter2 = t"a{1}b{2}c{3}d" +it = iter(t_iter2) +micropython.heap_lock() +try: + list(it) + print("FAIL: Iterator next") +except MemoryError: + print("OK: Iterator next") +micropython.heap_unlock() + +# __template__ builtin +strings2 = ("test",) * 5 +interps2 = ((42, "x", None, ""),) * 4 +micropython.heap_lock() +try: + __template__(strings2, interps2) + print("FAIL: __template__ builtin") +except MemoryError: + print("OK: __template__ builtin") +micropython.heap_unlock() + +# Format spec interpolation +width = 10 +t_fmt = t"{42:{width}d}" +micropython.heap_lock() +try: + str(t_fmt) + print("FAIL: Format spec interpolation") +except MemoryError: + print("OK: Format spec interpolation") +micropython.heap_unlock() + +# Debug format +x = 42 +t_debug = t"{x=}" +micropython.heap_lock() +try: + str(t_debug) + print("FAIL: Debug format") +except MemoryError: + print("OK: Debug format") +micropython.heap_unlock() + +# Conversion with format +obj = "test" +t_conv = t"{obj!r:>10}" +micropython.heap_lock() +try: + str(t_conv) + print("FAIL: Conversion + format") +except MemoryError: + print("OK: Conversion + format") +micropython.heap_unlock() + +# String conversion (s) +t_s = t"{'test'!s}" +micropython.heap_lock() +try: + str(t_s) + print("FAIL: s conversion") +except MemoryError: + print("OK: s conversion") +micropython.heap_unlock() + +# ASCII conversion (a) +# t_a = t"{'test'!a}" +# micropython.heap_lock() +# try: +# str(t_a) +# print("FAIL: a conversion") +# except MemoryError: +# print("OK: a conversion") +# micropython.heap_unlock() + +# Complex format spec +fill = "*" +align = ">" +width = 10 +precision = 2 +t_complex = t"{3.14159:{fill}{align}{width}.{precision}f}" +micropython.heap_lock() +try: + str(t_complex) + print("FAIL: Complex format spec") +except MemoryError: + print("OK: Complex format spec") +micropython.heap_unlock() + +# Simple expression +x = 42 +t_simple = t"{x}" +micropython.heap_lock() +try: + str(t_simple) + print("FAIL: Simple expression") +except MemoryError: + print("OK: Simple expression") +micropython.heap_unlock() + +# Interpolation creation +micropython.heap_lock() +try: + Interpolation("value", "expr", "r", ".2f") + print("FAIL: Interpolation creation") +except MemoryError: + print("OK: Interpolation creation") +micropython.heap_unlock() + +# Template repr +t = t"test" +micropython.heap_lock() +try: + repr(t) + print("FAIL: Template repr") +except MemoryError: + print("OK: Template repr") +micropython.heap_unlock() + +# Interpolation repr +i = Interpolation(42, "x") +micropython.heap_lock() +try: + repr(i) + print("FAIL: Interpolation repr") +except MemoryError: + print("OK: Interpolation repr") +micropython.heap_unlock() + +def exhaust_heap(limit): + allocations = [] + try: + for _ in range(limit): + allocations.append("x" * 1024) + except MemoryError: + pass + return allocations + +def test_many_interpolations_heap(): + # Pre-create variables before exhausting heap + x1, x2, x3, x4, x5, x6, x7, x8, x9 = 1, 2, 3, 4, 5, 6, 7, 8, 9 + micropython.heap_lock() + try: + t = t"{x1}{x2}{x3}{x4}{x5}{x6}{x7}{x8}{x9}" + print("FAIL: Many interpolations heap test") + except MemoryError: + print("OK: Many interpolations heap test") + micropython.heap_unlock() + +def test_template_str_heap(): + t = t"x{1}x{2}x{3}x{4}x" + micropython.heap_lock() + try: + s = str(t) + print("FAIL: Template str heap test") + except MemoryError: + print("OK: Template str heap test") + micropython.heap_unlock() + +def test_template_iter_heap(): + t = t"a{1}b{2}c" + micropython.heap_lock() + try: + parts = list(iter(t)) + print("FAIL: Template iter heap test") + except MemoryError: + print("OK: Template iter heap test") + micropython.heap_unlock() + +def test_template_concat_heap(): + t1 = t"first" + t2 = t"second" + micropython.heap_lock() + try: + t3 = t1 + t2 + print("FAIL: Template concat heap test") + except MemoryError: + print("OK: Template concat heap test") + micropython.heap_unlock() + +def test_format_spec_heap(): + width = 10 + t = t"{42:{width}d}" + micropython.heap_lock() + try: + s = str(t) + print("FAIL: Format spec heap test") + except MemoryError: + print("OK: Format spec heap test") + micropython.heap_unlock() + +def test_debug_format_heap(): + value = 42 + t = t"{value}" + micropython.heap_lock() + try: + s = str(t) + print("FAIL: Debug format heap test") + except MemoryError: + print("OK: Debug format heap test") + micropython.heap_unlock() + +print("\n=== Heap allocation failure tests ===") +test_many_interpolations_heap() +test_template_str_heap() +test_template_iter_heap() +test_template_concat_heap() +test_format_spec_heap() +test_debug_format_heap() + +# Additional tests from coverage.c +print("\n=== Coverage.c heap tests ===") + +# Test creating interpolation with heap locked (from coverage.c) +micropython.heap_lock() +try: + i = Interpolation(42, "x", None, None) + print("FAIL: Interpolation with heap locked") +except MemoryError: + print("OK: Interpolation creation with heap locked") +micropython.heap_unlock() + +# Test parsing expression with heap locked (from coverage.c) +micropython.heap_lock() +try: + t = eval('t"{x + 1}"') + print("FAIL: Parse with heap locked") +except Exception as e: + if type(e).__name__ == "MemoryError": + print("OK: Parse with heap locked") + else: + print(f"Parse with heap locked: {type(e).__name__}") +micropython.heap_unlock() + +# Test lexer allocation failure for t-string expression parsing +print("\n=== Lexer/Parser allocation tests ===") + +# Wrap all tests in a try-catch to handle any unexpected errors +try: + # Test 1: Empty expression (tests tstring_expr_parser.c empty check) + try: + micropython.heap_lock() + try: + # This tests the empty expression path + exec("t'{}'") + print("FAIL: Empty expression with heap locked") + except Exception as e: + print(f"OK: Empty expression - {type(e).__name__}") + finally: + micropython.heap_unlock() + except: + pass + + # Test 2: Whitespace-only expression + try: + micropython.heap_lock() + try: + exec("t'{ }'") + print("FAIL: Whitespace expression with heap locked") + except Exception as e: + print(f"OK: Whitespace expression - {type(e).__name__}") + finally: + micropython.heap_unlock() + except: + pass + + # Test 3: Very complex expression to stress parser allocation + # Pre-create variables + x, y, z = 1, 2, 3 + try: + micropython.heap_lock() + try: + # Complex nested expression that requires many parse nodes + result = t'{[x*y for x in range(10) for y in range(10) if x+y > 5]}' + print("FAIL: Complex expression with heap locked") + except MemoryError: + print("OK: Complex expression parser allocation") + except Exception as e: + print(f"Complex expression: {type(e).__name__}") + finally: + micropython.heap_unlock() + except: + pass + + # Test 4: Expression parser lexer failure (simulates NULL lexer) + # This happens when memory is exhausted during lexer creation + try: + micropython.heap_lock() + try: + # Try to create t-string with expression during low memory + long_expr = "x" * 100 # Use a moderately long expression + exec(f"{long_expr} = 42; result = t'{{{long_expr}}}'") + print("FAIL: Lexer creation with heap locked") + except MemoryError: + print("OK: Lexer creation failure") + except Exception as e: + print(f"Lexer creation: {type(e).__name__}") + finally: + micropython.heap_unlock() + except: + pass + + # Test 5: Format spec and conversion parsing under memory pressure + val = 42 + try: + micropython.heap_lock() + try: + # This tests format spec node creation + result = t'{val!r:>10.2f}' + print("FAIL: Format spec with heap locked") + except MemoryError: + print("OK: Format spec allocation") + except Exception as e: + print(f"Format spec: {type(e).__name__}") + finally: + micropython.heap_unlock() + except: + pass + + # Test 6: Debug format with memory pressure + x = 100 + try: + micropython.heap_lock() + try: + result = t'{x=:.2f}' + print("FAIL: Debug format with heap locked") + except MemoryError: + print("OK: Debug format allocation") + except Exception as e: + print(f"Debug format: {type(e).__name__}") + finally: + micropython.heap_unlock() + except: + pass + + # Test 7: Multiple interpolations causing allocation failure + vals = list(range(20)) + try: + micropython.heap_lock() + try: + # Many interpolations to stress allocation + result = t'{vals[0]}{vals[1]}{vals[2]}{vals[3]}{vals[4]}{vals[5]}{vals[6]}{vals[7]}{vals[8]}{vals[9]}' + print("FAIL: Many interpolations with heap locked") + except MemoryError: + print("OK: Many interpolations allocation") + except Exception as e: + print(f"Many interpolations: {type(e).__name__}") + finally: + micropython.heap_unlock() + except: + pass + + # Test 8: Template string size limit (tests overflow check) + # Note: We can't directly test integer overflow, but we can test large templates + print("Template size limit: Tested via exec in coverage tests") + + # Test 9: Compile-time parsing under allocation failure + try: + micropython.heap_lock() + try: + compile("t'{x}'", "", "exec") + print("FAIL: Compile-time parsing under heap lock") + except MemoryError: + print("OK: Compile-time parsing under heap lock") + finally: + micropython.heap_unlock() + except: + pass + +except Exception as e: + # Catch any unexpected errors from the tests + print(f"\nTest error: {type(e).__name__}: {e}") + # Ensure heap is unlocked + try: + micropython.heap_unlock() + except: + pass + +print("\n=== Tests completed ===") diff --git a/tests/micropython/heapalloc_fail_tstring.py.exp b/tests/micropython/heapalloc_fail_tstring.py.exp new file mode 100644 index 00000000000..3bc90ef418a --- /dev/null +++ b/tests/micropython/heapalloc_fail_tstring.py.exp @@ -0,0 +1,42 @@ +OK: Template creation +OK: Multi string concat +OK: Mixed constructor +OK: Template.__str__() +OK: Template.values +OK: Large values array +OK: Template concatenation +OK: Template iterator +OK: Iterator next +OK: __template__ builtin +OK: Format spec interpolation +OK: Debug format +OK: Conversion + format +OK: s conversion +OK: Complex format spec +OK: Simple expression +OK: Interpolation creation +OK: Template repr +OK: Interpolation repr + +=== Heap allocation failure tests === +OK: Many interpolations heap test +OK: Template str heap test +OK: Template iter heap test +OK: Template concat heap test +OK: Format spec heap test +OK: Debug format heap test + +=== Coverage.c heap tests === +OK: Interpolation creation with heap locked +OK: Parse with heap locked + +=== Lexer/Parser allocation tests === +OK: Complex expression parser allocation +OK: Lexer creation failure +OK: Format spec allocation +OK: Debug format allocation +OK: Many interpolations allocation +Template size limit: Tested via exec in coverage tests +OK: Compile-time parsing under heap lock + +=== Tests completed === diff --git a/tests/micropython/heapalloc_fail_tuple.py b/tests/micropython/heapalloc_fail_tuple.py index de79385e3e3..11718a8107b 100644 --- a/tests/micropython/heapalloc_fail_tuple.py +++ b/tests/micropython/heapalloc_fail_tuple.py @@ -1,6 +1,12 @@ # test handling of failed heap allocation with tuple -import micropython +try: + import micropython + + micropython.heap_lock +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit # create tuple x = 1 diff --git a/tests/micropython/heapalloc_inst_call.py b/tests/micropython/heapalloc_inst_call.py index 14d8826bf06..f78aa3cf877 100644 --- a/tests/micropython/heapalloc_inst_call.py +++ b/tests/micropython/heapalloc_inst_call.py @@ -1,6 +1,13 @@ # Test that calling clazz.__call__() with up to at least 3 arguments # doesn't require heap allocation. -import micropython + +try: + import micropython + + micropython.heap_lock +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit class Foo0: diff --git a/tests/micropython/heapalloc_int_from_bytes.py b/tests/micropython/heapalloc_int_from_bytes.py index 5fe50443ae3..3310ea95d14 100644 --- a/tests/micropython/heapalloc_int_from_bytes.py +++ b/tests/micropython/heapalloc_int_from_bytes.py @@ -1,6 +1,13 @@ # Test that int.from_bytes() for small number of bytes generates # small int. -import micropython + +try: + import micropython + + micropython.heap_lock +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit micropython.heap_lock() print(int.from_bytes(b"1", "little")) diff --git a/tests/micropython/heapalloc_slice.py b/tests/micropython/heapalloc_slice.py new file mode 100644 index 00000000000..62d96595c71 --- /dev/null +++ b/tests/micropython/heapalloc_slice.py @@ -0,0 +1,18 @@ +# slice operations that don't require allocation +try: + from micropython import heap_lock, heap_unlock +except (ImportError, AttributeError): + heap_lock = heap_unlock = lambda: 0 + +b = bytearray(range(10)) + +m = memoryview(b) + +heap_lock() + +b[3:5] = b"aa" +m[5:7] = b"bb" + +heap_unlock() + +print(b) diff --git a/tests/micropython/heapalloc_str.py b/tests/micropython/heapalloc_str.py index 39aa56ccd78..6372df5d37b 100644 --- a/tests/micropython/heapalloc_str.py +++ b/tests/micropython/heapalloc_str.py @@ -1,5 +1,12 @@ # String operations which don't require allocation -import micropython + +try: + import micropython + + micropython.heap_lock +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit micropython.heap_lock() diff --git a/tests/micropython/heapalloc_super.py b/tests/micropython/heapalloc_super.py index 51afae3d83d..6839eee330a 100644 --- a/tests/micropython/heapalloc_super.py +++ b/tests/micropython/heapalloc_super.py @@ -1,5 +1,12 @@ # test super() operations which don't require allocation -import micropython + +try: + import micropython + + micropython.heap_lock +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit # Check for stackless build, which can't call functions without # allocating a frame on heap. diff --git a/tests/micropython/heapalloc_traceback.py b/tests/micropython/heapalloc_traceback.py index 4c5f99afeeb..89fa37a48cc 100644 --- a/tests/micropython/heapalloc_traceback.py +++ b/tests/micropython/heapalloc_traceback.py @@ -1,10 +1,9 @@ # test that we can generate a traceback without allocating -import micropython import sys try: - import io + import io, micropython except ImportError: print("SKIP") raise SystemExit @@ -31,13 +30,13 @@ def test(): # call test() with heap allocation disabled test() -# print the exception that was raised +# print the exception that was raised. Use repr() to make the whitespace visible. buf = io.StringIO() sys.print_exception(global_exc, buf) for l in buf.getvalue().split("\n"): - # uPy on pyboard prints as file, so remove filename. + # MicroPython on pyboard prints as file, so remove filename. if l.startswith(" File "): l = l.split('"') - print(l[0], l[2]) + print(repr(l[0]), repr(l[2])) else: - print(l) + print(repr(l)) diff --git a/tests/micropython/heapalloc_traceback.py.exp b/tests/micropython/heapalloc_traceback.py.exp index 71929db93d5..fa30fa22080 100644 --- a/tests/micropython/heapalloc_traceback.py.exp +++ b/tests/micropython/heapalloc_traceback.py.exp @@ -1,5 +1,5 @@ StopIteration -Traceback (most recent call last): - File , line 25, in test -StopIteration: - +'Traceback (most recent call last):' +' File ' ', line 24, in test' +'StopIteration: ' +'' diff --git a/tests/micropython/heapalloc_traceback.py.native.exp b/tests/micropython/heapalloc_traceback.py.native.exp new file mode 100644 index 00000000000..851eb5c7806 --- /dev/null +++ b/tests/micropython/heapalloc_traceback.py.native.exp @@ -0,0 +1,3 @@ +StopIteration +'StopIteration: ' +'' diff --git a/tests/micropython/heapalloc_yield_from.py b/tests/micropython/heapalloc_yield_from.py index 95071718908..9d4f8c6940f 100644 --- a/tests/micropython/heapalloc_yield_from.py +++ b/tests/micropython/heapalloc_yield_from.py @@ -1,6 +1,12 @@ # Check that yield-from can work without heap allocation -import micropython +try: + import micropython + + micropython.heap_lock +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit # Yielding from a function generator diff --git a/tests/micropython/import_mpy_invalid.py b/tests/micropython/import_mpy_invalid.py index 89fdf44839b..f928d45c791 100644 --- a/tests/micropython/import_mpy_invalid.py +++ b/tests/micropython/import_mpy_invalid.py @@ -22,7 +22,9 @@ def readinto(self, buf): return n def ioctl(self, req, arg): - return 0 + if req == 4: # MP_STREAM_CLOSE + return 0 + return -1 class UserFS: diff --git a/tests/micropython/import_mpy_native.py b/tests/micropython/import_mpy_native.py index 8f5de25a0b2..b7908b5a629 100644 --- a/tests/micropython/import_mpy_native.py +++ b/tests/micropython/import_mpy_native.py @@ -28,7 +28,9 @@ def readinto(self, buf): return n def ioctl(self, req, arg): - return 0 + if req == 4: # MP_STREAM_CLOSE + return 0 + return -1 class UserFS: @@ -51,11 +53,12 @@ def open(self, path, mode): # these are the test .mpy files -valid_header = bytes([77, 6, mpy_arch, 31]) +small_int_bits = 30 +valid_header = bytes([77, 6, (mpy_arch & 0x3F), small_int_bits]) # fmt: off user_files = { # bad architecture (mpy_arch needed for sub-version) - '/mod0.mpy': bytes([77, 6, 0xfc | mpy_arch, 31]), + '/mod0.mpy': bytes([77, 6, 0xfc | (mpy_arch & 3), small_int_bits]), # test loading of viper and asm '/mod1.mpy': valid_header + ( diff --git a/tests/micropython/import_mpy_native_gc.py b/tests/micropython/import_mpy_native_gc.py index bdeb612b492..bfc4e1c107d 100644 --- a/tests/micropython/import_mpy_native_gc.py +++ b/tests/micropython/import_mpy_native_gc.py @@ -22,7 +22,9 @@ def readinto(self, buf): return n def ioctl(self, req, arg): - return 0 + if req == 4: # MP_STREAM_CLOSE + return 0 + return -1 class UserFS: @@ -58,6 +60,8 @@ def open(self, path, mode): 0x1006: b"M\x06\x13\x1f\x03\x008build/test_armv6m.native.mpy\x00\x08add1\x00\x0cunused\x00\x8eb0\xe0\x00\x00\x00\x00\x00\x00\x02K\x03J{D\x9bX\x18hpG\xd0\x00\x00\x00\x00\x00\x00\x00\x10\xb5\x05K\x05I\x06J{D\x9aX[X\x10h\x02!\x1bi\x98G\x10\xbd\xb8\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x10\xb5\x06K\x06J{D\x9bX\x02!\x1ci\xdbh\x98G\x02!\x010\xa0G\x10\xbd\xc0F\x96\x00\x00\x00\x00\x00\x00\x00\xf7\xb5\x12O\x12K\x7fD\xfdX\x12Lki|D\x00\x93ChXh\x00\x9b\x98G\x0fK\x01\x90\x0fJ\xfbXnk\x1a`\x0eK!\x00\xffX\xb8\x88\xb0G!\x00V \x081\xb0G!\x00x\x88\x101\xb0G\x01\x98\x00\x9b\x98G(h\xfe\xbd\xc0Fr\x00\x00\x00\x00\x00\x00\x00R\x00\x00\x00\x08\x00\x00\x00@\xe2\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x00\x00\x00\x00\x1d\x00\x00\x00\x00\x00\x00\x00A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00P\x04\x11p\rr\tt\xafd\x01f\xadh\x01j\xafl\x01n\xff", # -march=xtensawin 0x2806: b"M\x06+\x1f\x03\x00>build/test_xtensawin.native.mpy\x00\x08add1\x00\x0cunused\x00\x8a\x12\x06\x16\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x006A\x00\x81\xf9\xff(\x08\x1d\xf0\x00\x006A\x00\x91\xfb\xff\x81\xf5\xff\xa8\t\x88H\x0c+\xe0\x08\x00-\n\x1d\xf0\x00\x006A\x00\x81\xf0\xff\xad\x02xH\x888\x0c+\xe0\x08\x00\x0c+\x1b\xaa\xe0\x07\x00-\n\x1d\xf06A\x00a\xe9\xff\x88\x122&\x05\xa2(\x01\xe0\x03\x00q\xe6\xff\x81\xea\xff\x92\xa7\x89\xa0\x99\x11H\xd6]\n\xb1\xe3\xff\xa2\x17\x02\x99\x08\xe0\x04\x00\xb1\xe2\xff\\j\xe0\x04\x00\xb1\xe1\xff\xa2\x17\x01\xe0\x04\x00\xad\x05\xe0\x03\x00(\x06\x1d\xf0p\x18\x04\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x11\x02\r\x04\x07\x06\x03\t\x0c\xaf\x01\x01\x03\xad\x05\x01\x07\xaf\t\x01\x0b\xff", + # -march=rv32imc + 0x2C06: b'M\x06/\x1f\x03\x00:build/test_rv32imc.native.mpy\x00\x08add1\x00\x0cunused\x00\x8fb\x97\x0f\x00\x00g\x80\x0f\x05\x97\x07\x00\x00\x83\xa7\x07\x0e\x88C\x82\x80\x97\x07\x00\x00\x83\xa7G\r\x17\x07\x00\x00\x03\'\xc7\r\x9cK\x08C\x89E\x82\x87A\x11\x97\x07\x00\x00\x83\xa7\xa7\x0b"\xc4\x80K\xdcG\x06\xc6\x89E\x82\x97\xa2\x87"D\xb2@\x89E\x05\x05A\x01\x82\x87\\A\x01\x11"\xcc\x17\x04\x00\x00\x03$$\tN\xc6\xc8C\x83)D\x01\x06\xce&\xcaJ\xc8R\xc4\x82\x99\x17\n\x00\x00\x03*\xca\x07\x03)D\x03\xaa\x84\xf9g\x03UJ\x00\x93\x87\x07$\x17\x07\x00\x00\x03\'\x07\x07\x1c\xc3\x97\x05\x00\x00\x93\x85\xe5\x03\x02\x99\x97\x05\x00\x00\x93\x85\xc5\x03\x13\x05`\x05\x02\x99\x03U*\x00\x97\x05\x00\x00\x93\x85%\x03\x02\x99&\x85\x82\x99\x08@\xf2@bD\xd2DBI\xb2I"J\x05a\x82\x80\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00\x00\x00\x00\x00,\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00P\x04\x11t\rv\xafx\xadz\t|\xafh\x01j\xadl\x01n\xafp\x01r\xff', } # Populate armv7m-derived archs based on armv6m. @@ -65,7 +69,7 @@ def open(self, path, mode): features0_file_contents[arch] = features0_file_contents[0x1006] # Check that a .mpy exists for the target (ignore sub-version in lookup). -sys_implementation_mpy = sys.implementation._mpy & ~(3 << 8) +sys_implementation_mpy = (sys.implementation._mpy & ~(3 << 8)) & 0xFFFF if sys_implementation_mpy not in features0_file_contents: print("SKIP") raise SystemExit diff --git a/tests/micropython/import_mpy_native_gc_module/Makefile b/tests/micropython/import_mpy_native_gc_module/Makefile index 935ba6b59fa..feccfc11762 100644 --- a/tests/micropython/import_mpy_native_gc_module/Makefile +++ b/tests/micropython/import_mpy_native_gc_module/Makefile @@ -3,9 +3,13 @@ MPY_DIR = ../../.. MOD = test_$(ARCH) SRC = test.c ARCH = x64 +PYTHON := $(command -v python3 2> /dev/null) +ifndef PYTHON +PYTHON = python +endif .PHONY: main main: all - $(Q)cat $(MOD).mpy | python -c 'import sys; print(sys.stdin.buffer.read())' + $(Q)cat $(MOD).mpy | $(PYTHON) -c 'import sys; print(sys.stdin.buffer.read())' include $(MPY_DIR)/py/dynruntime.mk diff --git a/tests/micropython/incomplete_exc.py b/tests/micropython/incomplete_exc.py new file mode 100644 index 00000000000..2aec9a47b3a --- /dev/null +++ b/tests/micropython/incomplete_exc.py @@ -0,0 +1,40 @@ +# Test raising an incomplete exception. + + +class C(Exception): + def __init__(self): + raise self + + +class C1(C): + pass + + +class B: + pass + + +class C2(B, Exception): + def __init__(self): + raise self + + +class C3(Exception, B): + def __init__(self): + raise self + + +class D(Exception): + pass + + +class C4(D): + def __init__(self): + raise self + + +for cls in C, C1, C2, C3, C4: + try: + cls() + except TypeError as e: + print("TypeError") diff --git a/tests/extmod/machine1.py.exp b/tests/micropython/incomplete_exc.py.exp similarity index 57% rename from tests/extmod/machine1.py.exp rename to tests/micropython/incomplete_exc.py.exp index 25048596908..f2e9c12f7f9 100644 --- a/tests/extmod/machine1.py.exp +++ b/tests/micropython/incomplete_exc.py.exp @@ -1,6 +1,3 @@ -<8-bit memory> -ValueError -ValueError TypeError TypeError TypeError diff --git a/tests/micropython/io_badlength.py b/tests/micropython/io_badlength.py new file mode 100644 index 00000000000..646a2cd9241 --- /dev/null +++ b/tests/micropython/io_badlength.py @@ -0,0 +1,35 @@ +# Test when a use IOBase class has write/readinto which returns more data than +# requested (https://github.com/micropython/micropython/issues/18845) + +try: + import io, json +except: + print("SKIP") + raise SystemExit + + +class S(io.IOBase): + def write(self, buf): + assert len(buf) >= 0 + return 2 + + def ioctl(self, cmd, arg): + return 0 + + def readinto(self, buf): + assert len(buf) >= 0 + return 3 + + +try: + print("abc", file=S()) + print("write OK") +except OSError as e: + print("write failed, errno", e.errno) + +buf = bytearray(1) +try: + json.load(S()) + print("readinto OK") +except OSError as e: + print("read failed, errno", e.errno) diff --git a/tests/micropython/io_badlength.py.exp b/tests/micropython/io_badlength.py.exp new file mode 100644 index 00000000000..bba660dd3c8 --- /dev/null +++ b/tests/micropython/io_badlength.py.exp @@ -0,0 +1,2 @@ +write failed, errno 5 +read failed, errno 5 diff --git a/tests/micropython/kbd_intr.py b/tests/micropython/kbd_intr.py index 81977aaa52f..e1ed7185ef0 100644 --- a/tests/micropython/kbd_intr.py +++ b/tests/micropython/kbd_intr.py @@ -1,10 +1,10 @@ # test the micropython.kbd_intr() function -import micropython - try: + import micropython + micropython.kbd_intr -except AttributeError: +except (ImportError, AttributeError): print("SKIP") raise SystemExit diff --git a/tests/micropython/meminfo.py b/tests/micropython/meminfo.py index 9df341fbb83..f4dd8fdb604 100644 --- a/tests/micropython/meminfo.py +++ b/tests/micropython/meminfo.py @@ -1,12 +1,14 @@ # tests meminfo functions in micropython module -import micropython +try: + import micropython -# these functions are not always available -if not hasattr(micropython, "mem_info"): + micropython.mem_info +except (ImportError, AttributeError): print("SKIP") -else: - micropython.mem_info() - micropython.mem_info(1) - micropython.qstr_info() - micropython.qstr_info(1) + raise SystemExit + +micropython.mem_info() +micropython.mem_info(1) +micropython.qstr_info() +micropython.qstr_info(1) diff --git a/tests/micropython/memstats.py b/tests/micropython/memstats.py index dee3a4ce2f2..0e2e7b1c0b3 100644 --- a/tests/micropython/memstats.py +++ b/tests/micropython/memstats.py @@ -1,17 +1,19 @@ # tests meminfo functions in micropython module -import micropython +try: + import micropython -# these functions are not always available -if not hasattr(micropython, "mem_total"): + micropython.mem_total +except (ImportError, AttributeError): print("SKIP") -else: - t = micropython.mem_total() - c = micropython.mem_current() - p = micropython.mem_peak() + raise SystemExit - l = list(range(10000)) +t = micropython.mem_total() +c = micropython.mem_current() +p = micropython.mem_peak() - print(micropython.mem_total() > t) - print(micropython.mem_current() > c) - print(micropython.mem_peak() > p) +l = list(range(10000)) + +print(micropython.mem_total() > t) +print(micropython.mem_current() > c) +print(micropython.mem_peak() > p) diff --git a/tests/micropython/native_fun_attrs_code.py b/tests/micropython/native_fun_attrs_code.py new file mode 100644 index 00000000000..d277a7b9b26 --- /dev/null +++ b/tests/micropython/native_fun_attrs_code.py @@ -0,0 +1,21 @@ +# Test MicroPython-specific restrictions of function.__code__ attribute. + +try: + (lambda: 0).__code__ +except AttributeError: + print("SKIP") + raise SystemExit + +import micropython + + +@micropython.native +def f_native(): + pass + + +# Can't access __code__ when function is native code. +try: + f_native.__code__ +except AttributeError: + print("AttributeError") diff --git a/tests/micropython/native_fun_attrs_code.py.exp b/tests/micropython/native_fun_attrs_code.py.exp new file mode 100644 index 00000000000..d169edffb4c --- /dev/null +++ b/tests/micropython/native_fun_attrs_code.py.exp @@ -0,0 +1 @@ +AttributeError diff --git a/tests/micropython/native_marshal.py b/tests/micropython/native_marshal.py new file mode 100644 index 00000000000..09a27a374b8 --- /dev/null +++ b/tests/micropython/native_marshal.py @@ -0,0 +1,45 @@ +# Test the marshal module in combination with native/viper functions. + +try: + import marshal + + (lambda: 0).__code__ +except (AttributeError, ImportError): + print("SKIP") + raise SystemExit + +import unittest + + +def f_native(): + @micropython.native + def g(): + pass + + return g + + +def f_viper(): + @micropython.viper + def g(): + pass + + return g + + +class Test(unittest.TestCase): + def test_native_function(self): + # Can't marshal a function with native code. + code = f_native.__code__ + with self.assertRaises(ValueError): + marshal.dumps(code) + + def test_viper_function(self): + # Can't marshal a function with viper code. + code = f_viper.__code__ + with self.assertRaises(ValueError): + marshal.dumps(code) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/micropython/native_with.py b/tests/micropython/native_with.py index 9c0b98af903..37c385a42f9 100644 --- a/tests/micropython/native_with.py +++ b/tests/micropython/native_with.py @@ -9,6 +9,7 @@ def __enter__(self): print("__enter__") def __exit__(self, a, b, c): + b = repr(b)[:10] # shorten exception to "NameError(" prefix for target compatibility print("__exit__", a, b, c) diff --git a/tests/micropython/native_with.py.exp b/tests/micropython/native_with.py.exp index 7e28663f6fc..b8083a4c66b 100644 --- a/tests/micropython/native_with.py.exp +++ b/tests/micropython/native_with.py.exp @@ -5,5 +5,5 @@ __exit__ None None None __init__ __enter__ 1 -__exit__ name 'fail' isn't defined None +__exit__ NameError( None NameError diff --git a/tests/micropython/opt_level.py b/tests/micropython/opt_level.py index dd5493a7a3c..789197d8825 100644 --- a/tests/micropython/opt_level.py +++ b/tests/micropython/opt_level.py @@ -1,4 +1,10 @@ -import micropython as micropython +# test micropython.opt_level() + +try: + import micropython +except ImportError: + print("SKIP") + raise SystemExit # check we can get and set the level micropython.opt_level(0) diff --git a/tests/micropython/opt_level_lineno.py b/tests/micropython/opt_level_lineno.py index d8253e54b41..fcfbe8643da 100644 --- a/tests/micropython/opt_level_lineno.py +++ b/tests/micropython/opt_level_lineno.py @@ -1,6 +1,23 @@ -import micropython as micropython +# test micropython.opt_level() and line numbers + +try: + import micropython +except ImportError: + print("SKIP") + raise SystemExit # check that level 3 doesn't store line numbers # the expected output is that any line is printed as "line 1" micropython.opt_level(3) -exec("try:\n xyz\nexcept NameError as er:\n import sys\n sys.print_exception(er)") + +# force bytecode emitter, because native emitter doesn't store line numbers +exec(""" +@micropython.bytecode +def f(): + try: + xyz + except NameError as er: + import sys + sys.print_exception(er) +f() +""") diff --git a/tests/micropython/opt_level_lineno.py.exp b/tests/micropython/opt_level_lineno.py.exp index 469b90ba793..b50f0628c81 100644 --- a/tests/micropython/opt_level_lineno.py.exp +++ b/tests/micropython/opt_level_lineno.py.exp @@ -1,3 +1,3 @@ Traceback (most recent call last): - File "", line 1, in + File "", line 1, in f NameError: name 'xyz' isn't defined diff --git a/tests/micropython/ringio.py b/tests/micropython/ringio.py index a0102ef63da..d82c04565bd 100644 --- a/tests/micropython/ringio.py +++ b/tests/micropython/ringio.py @@ -1,10 +1,10 @@ # Check that micropython.RingIO works correctly. -import micropython - try: + import micropython + micropython.RingIO -except AttributeError: +except (ImportError, AttributeError): print("SKIP") raise SystemExit @@ -46,3 +46,21 @@ micropython.RingIO(None) except TypeError as ex: print(type(ex)) + +try: + # Buffer may not be empty + micropython.RingIO(bytearray(0)) +except ValueError as ex: + print(type(ex)) + +try: + # Buffer may not be too small + micropython.RingIO(bytearray(1)) +except ValueError as ex: + print(type(ex)) + +try: + # Size may not be too small + micropython.RingIO(0) +except ValueError as ex: + print(type(ex)) diff --git a/tests/micropython/ringio.py.exp b/tests/micropython/ringio.py.exp index 65bae06472f..c391529a41e 100644 --- a/tests/micropython/ringio.py.exp +++ b/tests/micropython/ringio.py.exp @@ -14,3 +14,6 @@ b'\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01' 0 b'\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01' + + + diff --git a/tests/micropython/ringio_async.py b/tests/micropython/ringio_async.py index 2a4befc3527..0db146451c9 100644 --- a/tests/micropython/ringio_async.py +++ b/tests/micropython/ringio_async.py @@ -1,9 +1,7 @@ # Check that micropython.RingIO works correctly with asyncio.Stream. -import micropython - try: - import asyncio + import asyncio, micropython asyncio.StreamWriter micropython.RingIO diff --git a/tests/micropython/ringio_big.py b/tests/micropython/ringio_big.py new file mode 100644 index 00000000000..88d5d2cc165 --- /dev/null +++ b/tests/micropython/ringio_big.py @@ -0,0 +1,40 @@ +# Check that micropython.RingIO works correctly. + +try: + import micropython + + micropython.RingIO +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit + +results = [] + +try: + # The maximum possible size passed as an integer. + micropython.RingIO(65534) + + try: + # Size may not be too big + micropython.RingIO(65535) + except ValueError as ex: + results.append(type(ex)) + + # Allocate a buffer for use below. + buf_64k = memoryview(bytearray(65536)) + + # The maximum possible size passed as a buffer. + micropython.RingIO(buf_64k[:-1]) + + try: + # Buffer may not be too big + micropython.RingIO(buf_64k) + except ValueError as ex: + results.append(type(ex)) + +except MemoryError: + print("SKIP") + raise SystemExit + +for result in results: + print(result) diff --git a/tests/micropython/ringio_big.py.exp b/tests/micropython/ringio_big.py.exp new file mode 100644 index 00000000000..72af34b3838 --- /dev/null +++ b/tests/micropython/ringio_big.py.exp @@ -0,0 +1,2 @@ + + diff --git a/tests/micropython/schedule.py b/tests/micropython/schedule.py index 6a91459ea3d..e629edb3eb3 100644 --- a/tests/micropython/schedule.py +++ b/tests/micropython/schedule.py @@ -1,10 +1,12 @@ # test micropython.schedule() function - -import micropython +# this test should be manually kept in synch with +# tests/micrpython/schedule_sleep.py. try: + import micropython + micropython.schedule -except AttributeError: +except (ImportError, AttributeError): print("SKIP") raise SystemExit diff --git a/tests/micropython/schedule_sleep.py b/tests/micropython/schedule_sleep.py new file mode 100644 index 00000000000..9aadde7b084 --- /dev/null +++ b/tests/micropython/schedule_sleep.py @@ -0,0 +1,72 @@ +# test micropython.schedule() function +# this is the same as tests/micropython/schedule.py but the busy loops are +# replaced with sleep/sleep_ms which allows the test to succeed when run under +# the native emitter. + +try: + import micropython + import time + + micropython.schedule +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit + + +# Basic test of scheduling a function. + + +def callback(arg): + global done + print(arg) + done = True + + +done = False +micropython.schedule(callback, 1) +while not done: + time.sleep(0) + +# Test that callbacks can be scheduled from within a callback, but +# that they don't execute until the outer callback is finished. + + +def callback_inner(arg): + global done + print("inner") + done += 1 + + +def callback_outer(arg): + global done + micropython.schedule(callback_inner, 0) + # need a loop so that the VM can check for pending events + for i in range(2): + pass + print("outer") + done += 1 + + +done = 0 +micropython.schedule(callback_outer, 0) +while done != 2: + time.sleep(0) + +# Test that scheduling too many callbacks leads to an exception. To do this we +# must schedule from within a callback to guarantee that the scheduler is locked. + + +def callback(arg): + global done + try: + for i in range(100): + micropython.schedule(lambda x: x, None) + except RuntimeError: + print("RuntimeError") + done = True + + +done = False +micropython.schedule(callback, None) +while not done: + time.sleep_ms(0) diff --git a/tests/micropython/schedule_sleep.py.exp b/tests/micropython/schedule_sleep.py.exp new file mode 100644 index 00000000000..c4a3e1227e2 --- /dev/null +++ b/tests/micropython/schedule_sleep.py.exp @@ -0,0 +1,4 @@ +1 +outer +inner +RuntimeError diff --git a/tests/micropython/stack_use.py b/tests/micropython/stack_use.py index 266885d9d18..5d36fdca2fe 100644 --- a/tests/micropython/stack_use.py +++ b/tests/micropython/stack_use.py @@ -1,7 +1,11 @@ # tests stack_use function in micropython module -import micropython -if not hasattr(micropython, "stack_use"): +try: + import micropython + + micropython.stack_use +except (ImportError, AttributeError): print("SKIP") -else: - print(type(micropython.stack_use())) # output varies + raise SystemExit + +print(type(micropython.stack_use())) # output varies diff --git a/tests/micropython/test_normalize_newlines.py b/tests/micropython/test_normalize_newlines.py new file mode 100644 index 00000000000..f19aaa69a3f --- /dev/null +++ b/tests/micropython/test_normalize_newlines.py @@ -0,0 +1,14 @@ +# Test for normalize_newlines functionality +# This test verifies that test framework handles various newline combinations correctly + +# Note: This is more of an integration test since normalize_newlines is in the test framework +# The actual testing happens when this test is run through run-tests.py + +print("Testing newline handling") +print("Line 1\r\nLine 2") # Windows-style line ending - should be normalized +print("Line 3") # Normal line +print("Line 4") # Normal line +print("Line 5\nLine 6") # Unix-style line ending - already normalized + +# Test that literal \r in strings is preserved +print(repr("test\rstring")) # Should show 'test\rstring' not 'test\nstring' diff --git a/tests/micropython/test_normalize_newlines.py.exp b/tests/micropython/test_normalize_newlines.py.exp new file mode 100644 index 00000000000..c4395468cf1 --- /dev/null +++ b/tests/micropython/test_normalize_newlines.py.exp @@ -0,0 +1,8 @@ +Testing newline handling +Line 1 +Line 2 +Line 3 +Line 4 +Line 5 +Line 6 +'test\rstring' diff --git a/tests/micropython/viper_large_jump.py b/tests/micropython/viper_large_jump.py new file mode 100644 index 00000000000..1c5913dec1e --- /dev/null +++ b/tests/micropython/viper_large_jump.py @@ -0,0 +1,20 @@ +COUNT = 600 + + +try: + code = """ +@micropython.viper +def f() -> int: + x = 0 + while x < 10: +""" + for i in range(COUNT): + code += " x += 1\n" + code += " return x" + exec(code) +except MemoryError: + print("SKIP-TOO-LARGE") + raise SystemExit + + +print(f()) diff --git a/tests/micropython/viper_large_jump.py.exp b/tests/micropython/viper_large_jump.py.exp new file mode 100644 index 00000000000..e9f960cf4ac --- /dev/null +++ b/tests/micropython/viper_large_jump.py.exp @@ -0,0 +1 @@ +600 diff --git a/tests/micropython/viper_preserve_invariants.py b/tests/micropython/viper_preserve_invariants.py new file mode 100644 index 00000000000..26f6003aba7 --- /dev/null +++ b/tests/micropython/viper_preserve_invariants.py @@ -0,0 +1,39 @@ +RESULTS = [] + +LOAD_TEMPLATE_REG = """ +BUFFER = bytearray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) +OFFSET = 1 +@micropython.viper +def test_invariants_load{}_reg(buf: ptr{}, offset: int): + offset_copy1: int = offset + temporary1 = buf[offset] + offset_copy2: int = offset + temporary2 = buf[offset] + RESULTS.append(["LOAD{}", temporary1 == temporary2, offset == offset_copy1 == offset_copy2]) +test_invariants_load{}_reg(BUFFER, OFFSET) +""" + + +STORE_TEMPLATE_REG = """ +BUFFER = bytearray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) +OFFSET = 1 +@micropython.viper +def test_invariants_store{}_reg(buf: ptr{}, offset: int): + offset_copy: int = offset + value: uint = {} + buf[offset] = value + temporary = buf[offset] + RESULTS.append(["STORE{}", temporary == value, offset == offset_copy]) +test_invariants_store{}_reg(BUFFER, OFFSET) +""" + +try: + for width, value in ((8, 0x11), (16, 0x1111), (32, 0x11111111)): + exec(LOAD_TEMPLATE_REG.format(width, width, width, width, width)) + exec(STORE_TEMPLATE_REG.format(width, width, value, width, width, width)) +except MemoryError: + print("SKIP-TOO-LARGE") + raise SystemExit + +for line in RESULTS: + print(" ".join([str(i) for i in line])) diff --git a/tests/micropython/viper_preserve_invariants.py.exp b/tests/micropython/viper_preserve_invariants.py.exp new file mode 100644 index 00000000000..21c66252430 --- /dev/null +++ b/tests/micropython/viper_preserve_invariants.py.exp @@ -0,0 +1,6 @@ +LOAD8 True True +STORE8 True True +LOAD16 True True +STORE16 True True +LOAD32 True True +STORE32 True True diff --git a/tests/micropython/viper_ptr16_load_boundary.py b/tests/micropython/viper_ptr16_load_boundary.py new file mode 100644 index 00000000000..0d4c3105b68 --- /dev/null +++ b/tests/micropython/viper_ptr16_load_boundary.py @@ -0,0 +1,39 @@ +# Test boundary conditions for various architectures + +GET_TEMPLATE = """ +@micropython.viper +def get{off}(src: ptr16) -> uint: + return uint(src[{off}]) +print(hex(get{off}(buffer))) +""" + + +BIT_THRESHOLDS = (5, 8, 11, 12) +SIZE = 2 + + +@micropython.viper +def get_index(src: ptr16, i: int) -> int: + return src[i] + + +def data(start, len): + output = bytearray(len) + for idx in range(len): + output[idx] = (start + idx) & 0xFF + return output + + +buffer = bytearray((((1 << max(BIT_THRESHOLDS)) + 1) // 1024) * 1024) +val = 0 +for bit in BIT_THRESHOLDS: + print("---", bit) + pre, idx, post = ((1 << bit) - (2 * SIZE), (1 << bit) - (1 * SIZE), 1 << bit) + buffer[pre:post] = data(val, 3 * SIZE) + val = val + (3 * SIZE) + + pre, idx, post = pre // SIZE, idx // SIZE, post // SIZE + print(hex(get_index(buffer, pre)), hex(get_index(buffer, idx)), hex(get_index(buffer, post))) + exec(GET_TEMPLATE.format(off=pre)) + exec(GET_TEMPLATE.format(off=idx)) + exec(GET_TEMPLATE.format(off=post)) diff --git a/tests/micropython/viper_ptr16_load_boundary.py.exp b/tests/micropython/viper_ptr16_load_boundary.py.exp new file mode 100644 index 00000000000..56f1d322904 --- /dev/null +++ b/tests/micropython/viper_ptr16_load_boundary.py.exp @@ -0,0 +1,20 @@ +--- 5 +0x100 0x302 0x504 +0x100 +0x302 +0x504 +--- 8 +0x706 0x908 0xb0a +0x706 +0x908 +0xb0a +--- 11 +0xd0c 0xf0e 0x1110 +0xd0c +0xf0e +0x1110 +--- 12 +0x1312 0x1514 0x1716 +0x1312 +0x1514 +0x1716 diff --git a/tests/micropython/viper_ptr16_store_boundary.py b/tests/micropython/viper_ptr16_store_boundary.py new file mode 100644 index 00000000000..7c774d4d1ca --- /dev/null +++ b/tests/micropython/viper_ptr16_store_boundary.py @@ -0,0 +1,63 @@ +# Test boundary conditions for various architectures + +SET_TEMPLATE = """ +@micropython.viper +def set{off}(dest: ptr16): + saved = dest + dest[{off}] = {val} + assert int(saved) == int(dest) +""" + +BIT_THRESHOLDS = (5, 8, 11, 12) +SIZE = 2 +MASK = (1 << (8 * SIZE)) - 1 + +next_int = 1 +test_buffer = bytearray(SIZE) + + +def next_value() -> uint: + global next_int + global test_buffer + for index in range(1, SIZE): + test_buffer[index - 1] = test_buffer[index] + test_buffer[SIZE - 1] = next_int + next_int += 1 + output = 0 + for byte in test_buffer: + output = (output << 8) | byte + return output & MASK + + +def get_index(src, i): + return src[i * SIZE] + (src[(i * SIZE) + 1] << 8) + + +@micropython.viper +def set_index(dest: ptr16, i: int, val: uint): + saved = dest + dest[i] = val + assert int(saved) == int(dest) + + +try: + buffer = bytearray((((1 << max(BIT_THRESHOLDS)) // 1024) + 1) * 1024) + + for bit in BIT_THRESHOLDS: + offset = (1 << bit) - (2 * SIZE) + for index in range(0, 3 * SIZE, SIZE): + exec(SET_TEMPLATE.format(off=(offset + index) // SIZE, val=next_value())) +except MemoryError: + print("SKIP-TOO-LARGE") + raise SystemExit + + +for bit in BIT_THRESHOLDS: + print("---", bit) + offset = (1 << bit) - (2 * SIZE) + for index in range(0, 3 * SIZE, SIZE): + globals()["set{}".format((offset + index) // SIZE)](buffer) + print(hex(get_index(buffer, (offset + index) // SIZE))) + for index in range(0, 3 * SIZE, SIZE): + set_index(buffer, (offset + index) // SIZE, next_value()) + print(hex(get_index(buffer, (offset + index) // SIZE))) diff --git a/tests/micropython/viper_ptr16_store_boundary.py.exp b/tests/micropython/viper_ptr16_store_boundary.py.exp new file mode 100644 index 00000000000..007a50b3eda --- /dev/null +++ b/tests/micropython/viper_ptr16_store_boundary.py.exp @@ -0,0 +1,28 @@ +--- 5 +0x1 +0x102 +0x203 +0xc0d +0xd0e +0xe0f +--- 8 +0x304 +0x405 +0x506 +0xf10 +0x1011 +0x1112 +--- 11 +0x607 +0x708 +0x809 +0x1213 +0x1314 +0x1415 +--- 12 +0x90a +0xa0b +0xb0c +0x1516 +0x1617 +0x1718 diff --git a/tests/micropython/viper_ptr32_load_boundary.py b/tests/micropython/viper_ptr32_load_boundary.py new file mode 100644 index 00000000000..971d1113c49 --- /dev/null +++ b/tests/micropython/viper_ptr32_load_boundary.py @@ -0,0 +1,39 @@ +# Test boundary conditions for various architectures + +GET_TEMPLATE = """ +@micropython.viper +def get{off}(src: ptr32) -> uint: + return uint(src[{off}]) +print(hex(get{off}(buffer))) +""" + + +BIT_THRESHOLDS = (5, 8, 11, 12) +SIZE = 4 + + +@micropython.viper +def get_index(src: ptr32, i: int) -> int: + return src[i] + + +def data(start, len): + output = bytearray(len) + for idx in range(len): + output[idx] = (start + idx) & 0xFF + return output + + +buffer = bytearray((((1 << max(BIT_THRESHOLDS)) + 1) // 1024) * 1024) +val = 0 +for bit in BIT_THRESHOLDS: + print("---", bit) + pre, idx, post = (((1 << bit) - (2 * SIZE)), ((1 << bit) - (1 * SIZE)), (1 << bit)) + buffer[pre:post] = data(val, 3 * SIZE) + val = val + (3 * SIZE) + + pre, idx, post = pre // SIZE, idx // SIZE, post // SIZE + print(hex(get_index(buffer, pre)), hex(get_index(buffer, idx)), hex(get_index(buffer, post))) + exec(GET_TEMPLATE.format(off=pre)) + exec(GET_TEMPLATE.format(off=idx)) + exec(GET_TEMPLATE.format(off=post)) diff --git a/tests/micropython/viper_ptr32_load_boundary.py.exp b/tests/micropython/viper_ptr32_load_boundary.py.exp new file mode 100644 index 00000000000..1e22a8b3613 --- /dev/null +++ b/tests/micropython/viper_ptr32_load_boundary.py.exp @@ -0,0 +1,20 @@ +--- 5 +0x3020100 0x7060504 0xb0a0908 +0x3020100 +0x7060504 +0xb0a0908 +--- 8 +0xf0e0d0c 0x13121110 0x17161514 +0xf0e0d0c +0x13121110 +0x17161514 +--- 11 +0x1b1a1918 0x1f1e1d1c 0x23222120 +0x1b1a1918 +0x1f1e1d1c +0x23222120 +--- 12 +0x27262524 0x2b2a2928 0x2f2e2d2c +0x27262524 +0x2b2a2928 +0x2f2e2d2c diff --git a/tests/micropython/viper_ptr32_store_boundary.py b/tests/micropython/viper_ptr32_store_boundary.py new file mode 100644 index 00000000000..96ca74ad3ca --- /dev/null +++ b/tests/micropython/viper_ptr32_store_boundary.py @@ -0,0 +1,68 @@ +# Test boundary conditions for various architectures + +SET_TEMPLATE = """ +@micropython.viper +def set{off}(dest: ptr32): + saved = dest + dest[{off}] = {val} + assert int(saved) == int(dest) +""" + +BIT_THRESHOLDS = (5, 8, 11, 12) +SIZE = 4 +MASK = (1 << (8 * SIZE)) - 1 + +next_int = 1 +test_buffer = bytearray(SIZE) + + +def next_value() -> uint: + global next_int + global test_buffer + for index in range(1, SIZE): + test_buffer[index - 1] = test_buffer[index] + test_buffer[SIZE - 1] = next_int + next_int += 1 + output = 0 + for byte in test_buffer: + output = (output << 8) | byte + return output & MASK + + +def get_index(src, i): + return ( + src[i * SIZE] + + (src[(i * SIZE) + 1] << 8) + + (src[(i * SIZE) + 2] << 16) + + (src[(i * SIZE) + 3] << 24) + ) + + +@micropython.viper +def set_index(dest: ptr32, i: int, val: uint): + saved = dest + dest[i] = val + assert int(dest) == int(saved) + + +try: + buffer = bytearray((((1 << max(BIT_THRESHOLDS)) // 1024) + 1) * 1024) + + for bit in BIT_THRESHOLDS: + offset = (1 << bit) - (2 * SIZE) + for index in range(0, 3 * SIZE, SIZE): + exec(SET_TEMPLATE.format(off=(offset + index) // SIZE, val=next_value())) +except MemoryError: + print("SKIP-TOO-LARGE") + raise SystemExit + + +for bit in BIT_THRESHOLDS: + print("---", bit) + offset = (1 << bit) - (2 * SIZE) + for index in range(0, 3 * SIZE, SIZE): + globals()["set{}".format((offset + index) // SIZE)](buffer) + print(hex(get_index(buffer, (offset + index) // SIZE))) + for index in range(0, 3 * SIZE, SIZE): + set_index(buffer, (offset + index) // SIZE, next_value()) + print(hex(get_index(buffer, (offset + index) // SIZE))) diff --git a/tests/micropython/viper_ptr32_store_boundary.py.exp b/tests/micropython/viper_ptr32_store_boundary.py.exp new file mode 100644 index 00000000000..7a9a5162474 --- /dev/null +++ b/tests/micropython/viper_ptr32_store_boundary.py.exp @@ -0,0 +1,28 @@ +--- 5 +0x1 +0x102 +0x10203 +0xa0b0c0d +0xb0c0d0e +0xc0d0e0f +--- 8 +0x1020304 +0x2030405 +0x3040506 +0xd0e0f10 +0xe0f1011 +0xf101112 +--- 11 +0x4050607 +0x5060708 +0x6070809 +0x10111213 +0x11121314 +0x12131415 +--- 12 +0x708090a +0x8090a0b +0x90a0b0c +0x13141516 +0x14151617 +0x15161718 diff --git a/tests/micropython/viper_ptr8_load_boundary.py b/tests/micropython/viper_ptr8_load_boundary.py new file mode 100644 index 00000000000..57e06da5709 --- /dev/null +++ b/tests/micropython/viper_ptr8_load_boundary.py @@ -0,0 +1,38 @@ +# Test boundary conditions for various architectures + +GET_TEMPLATE = """ +@micropython.viper +def get{off}(src: ptr8) -> uint: + return uint(src[{off}]) +print(hex(get{off}(buffer))) +""" + + +BIT_THRESHOLDS = (5, 8, 11, 12) +SIZE = 1 + + +@micropython.viper +def get_index(src: ptr8, i: int) -> int: + return src[i] + + +def data(start, len): + output = bytearray(len) + for idx in range(len): + output[idx] = (start + idx) & 0xFF + return output + + +buffer = bytearray((((1 << max(BIT_THRESHOLDS)) + 1) // 1024) * 1024) +val = 0 +for bit in BIT_THRESHOLDS: + print("---", bit) + pre, idx, post = (((1 << bit) - (2 * SIZE)), ((1 << bit) - (1 * SIZE)), (1 << bit)) + buffer[pre:post] = data(val, 3 * SIZE) + val = val + (3 * SIZE) + + print(hex(get_index(buffer, pre)), hex(get_index(buffer, idx)), hex(get_index(buffer, post))) + exec(GET_TEMPLATE.format(off=pre)) + exec(GET_TEMPLATE.format(off=idx)) + exec(GET_TEMPLATE.format(off=post)) diff --git a/tests/micropython/viper_ptr8_load_boundary.py.exp b/tests/micropython/viper_ptr8_load_boundary.py.exp new file mode 100644 index 00000000000..a0e423686b8 --- /dev/null +++ b/tests/micropython/viper_ptr8_load_boundary.py.exp @@ -0,0 +1,20 @@ +--- 5 +0x0 0x1 0x2 +0x0 +0x1 +0x2 +--- 8 +0x3 0x4 0x5 +0x3 +0x4 +0x5 +--- 11 +0x6 0x7 0x8 +0x6 +0x7 +0x8 +--- 12 +0x9 0xa 0xb +0x9 +0xa +0xb diff --git a/tests/micropython/viper_ptr8_store_boundary.py b/tests/micropython/viper_ptr8_store_boundary.py new file mode 100644 index 00000000000..68b76fd598b --- /dev/null +++ b/tests/micropython/viper_ptr8_store_boundary.py @@ -0,0 +1,63 @@ +# Test boundary conditions for various architectures + +SET_TEMPLATE = """ +@micropython.viper +def set{off}(dest: ptr8): + saved = dest + dest[{off}] = {val} + assert int(saved) == int(dest) +""" + +BIT_THRESHOLDS = (5, 8, 11, 12) +SIZE = 1 +MASK = (1 << (8 * SIZE)) - 1 + +next_int = 1 +test_buffer = bytearray(SIZE) + + +def next_value() -> uint: + global next_int + global test_buffer + for index in range(1, SIZE): + test_buffer[index - 1] = test_buffer[index] + test_buffer[SIZE - 1] = next_int + next_int += 1 + output = 0 + for byte in test_buffer: + output = (output << 8) | byte + return output & MASK + + +def get_index(src: ptr8, i: int): + return src[i] + + +@micropython.viper +def set_index(dest: ptr8, i: int, val: uint): + saved = dest + dest[i] = val + assert int(dest) == int(saved) + + +try: + buffer = bytearray((((1 << max(BIT_THRESHOLDS)) // 1024) + 1) * 1024) + + for bit in BIT_THRESHOLDS: + offset = (1 << bit) - (2 * SIZE) + for index in range(0, 3 * SIZE, SIZE): + exec(SET_TEMPLATE.format(off=(offset + index) // SIZE, val=next_value())) +except MemoryError: + print("SKIP-TOO-LARGE") + raise SystemExit + + +for bit in BIT_THRESHOLDS: + print("---", bit) + offset = (1 << bit) - (2 * SIZE) + for index in range(0, 3 * SIZE, SIZE): + globals()["set{}".format((offset + index) // SIZE)](buffer) + print(hex(get_index(buffer, (offset + index) // SIZE))) + for index in range(0, 3 * SIZE, SIZE): + set_index(buffer, (offset + index) // SIZE, next_value()) + print(hex(get_index(buffer, (offset + index) // SIZE))) diff --git a/tests/micropython/viper_ptr8_store_boundary.py.exp b/tests/micropython/viper_ptr8_store_boundary.py.exp new file mode 100644 index 00000000000..621295d81a8 --- /dev/null +++ b/tests/micropython/viper_ptr8_store_boundary.py.exp @@ -0,0 +1,28 @@ +--- 5 +0x1 +0x2 +0x3 +0xd +0xe +0xf +--- 8 +0x4 +0x5 +0x6 +0x10 +0x11 +0x12 +--- 11 +0x7 +0x8 +0x9 +0x13 +0x14 +0x15 +--- 12 +0xa +0xb +0xc +0x16 +0x17 +0x18 diff --git a/tests/micropython/viper_with.py b/tests/micropython/viper_with.py index 40fbf6fb315..833a6babe7d 100644 --- a/tests/micropython/viper_with.py +++ b/tests/micropython/viper_with.py @@ -9,6 +9,7 @@ def __enter__(self): print("__enter__") def __exit__(self, a, b, c): + b = repr(b)[:10] # shorten exception to "NameError(" prefix for target compatibility print("__exit__", a, b, c) diff --git a/tests/micropython/viper_with.py.exp b/tests/micropython/viper_with.py.exp index 7e28663f6fc..b8083a4c66b 100644 --- a/tests/micropython/viper_with.py.exp +++ b/tests/micropython/viper_with.py.exp @@ -5,5 +5,5 @@ __exit__ None None None __init__ __enter__ 1 -__exit__ name 'fail' isn't defined None +__exit__ NameError( None NameError diff --git a/tests/misc/cexample_subclass.py b/tests/misc/cexample_subclass.py index 9f52a2c737a..6857788e5dc 100644 --- a/tests/misc/cexample_subclass.py +++ b/tests/misc/cexample_subclass.py @@ -2,6 +2,7 @@ try: from cexample import AdvancedTimer + import time # used to skip this test on minimal unix variant except ImportError: print("SKIP") raise SystemExit diff --git a/tests/misc/non_compliant.py b/tests/misc/non_compliant.py index da90f90ac3e..54ad2960e49 100644 --- a/tests/misc/non_compliant.py +++ b/tests/misc/non_compliant.py @@ -39,7 +39,7 @@ except NotImplementedError: print("NotImplementedError") -# uPy raises TypeError, should be ValueError +# MicroPython raises TypeError, should be ValueError try: "%c" % b"\x01\x02" except (TypeError, ValueError): @@ -63,12 +63,6 @@ except NotImplementedError: print("NotImplementedError") -# str.endswith(s, start) not implemented -try: - "abc".endswith("c", 1) -except NotImplementedError: - print("NotImplementedError") - # str subscr with step!=1 not implemented try: print("abc"[1:2:3]) @@ -105,10 +99,10 @@ except NotImplementedError: print("NotImplementedError") -# struct pack with too many args, not checked by uPy +# struct pack with too many args, not checked by MicroPython print(struct.pack("bb", 1, 2, 3)) -# struct pack with too few args, not checked by uPy +# struct pack with too few args, not checked by MicroPython print(struct.pack("bb", 1)) # array slice assignment with unsupported RHS diff --git a/tests/misc/non_compliant.py.exp b/tests/misc/non_compliant.py.exp index 3f15a144060..deefe78a712 100644 --- a/tests/misc/non_compliant.py.exp +++ b/tests/misc/non_compliant.py.exp @@ -13,7 +13,6 @@ NotImplementedError NotImplementedError NotImplementedError NotImplementedError -NotImplementedError b'\x01\x02' b'\x01\x00' NotImplementedError diff --git a/tests/misc/non_compliant_lexer.py b/tests/misc/non_compliant_lexer.py index e1c21f3d713..04c605953e7 100644 --- a/tests/misc/non_compliant_lexer.py +++ b/tests/misc/non_compliant_lexer.py @@ -11,7 +11,7 @@ def test(code): print("NotImplementedError") -# uPy requires spaces between literal numbers and keywords, CPy doesn't +# MPy requires spaces between literal numbers and keywords, CPy doesn't try: eval("1and 0") except SyntaxError: diff --git a/tests/misc/print_exception.py b/tests/misc/print_exception.py index 1d196d6ab16..fdf5711c578 100644 --- a/tests/misc/print_exception.py +++ b/tests/misc/print_exception.py @@ -1,3 +1,5 @@ +# Test sys.print_exception (MicroPython) / traceback.print_exception (CPython). + try: import io import sys @@ -18,11 +20,11 @@ def print_exc(e): print_exception(e, buf) s = buf.getvalue() for l in s.split("\n"): - # uPy on pyboard prints as file, so remove filename. + # MPy on pyboard prints as file, so remove filename. if l.startswith(" File "): l = l.split('"') print(l[0], l[2]) - # uPy and CPy tracebacks differ in that CPy prints a source line for + # MPy and CPy tracebacks differ in that CPy prints a source line for # each traceback entry. In this case, we know that offending line # has 4-space indent, so filter it out. elif not l.startswith(" "): @@ -69,7 +71,7 @@ def g(): except Exception as e: print("reraise") print_exc(e) - raise + raise e except Exception as e: print("caught") print_exc(e) @@ -87,7 +89,7 @@ def f(): except Exception as e: print_exc(e) -# Test non-stream object passed as output object, only valid for uPy +# Test non-stream object passed as output object, only valid for MicroPython if hasattr(sys, "print_exception"): try: sys.print_exception(Exception, 1) diff --git a/tests/misc/print_exception.py.native.exp b/tests/misc/print_exception.py.native.exp new file mode 100644 index 00000000000..59e856ae3c4 --- /dev/null +++ b/tests/misc/print_exception.py.native.exp @@ -0,0 +1,18 @@ +caught +Exception: msg + +caught +Exception: fail + +finally +caught +Exception: fail + +reraise +Exception: fail + +caught +Exception: fail + +AttributeError: 'function' object has no attribute 'X' + diff --git a/tests/misc/rge_sm.py b/tests/misc/rge_sm.py index 5e071687c49..9e74c3b7a79 100644 --- a/tests/misc/rge_sm.py +++ b/tests/misc/rge_sm.py @@ -39,14 +39,6 @@ def solve(self, finishtime): if not self.iterate(): break - def solveNSteps(self, nSteps): - for i in range(nSteps): - if not self.iterate(): - break - - def series(self): - return zip(*self.Trajectory) - # 1-loop RGES for the main parameters of the SM # couplings are: g1, g2, g3 of U(1), SU(2), SU(3); yt (top Yukawa), lambda (Higgs quartic) @@ -79,61 +71,32 @@ def series(self): ) -def drange(start, stop, step): - r = start - while r < stop: - yield r - r += step - - -def phaseDiagram(system, trajStart, trajPlot, h=0.1, tend=1.0, range=1.0): - tstart = 0.0 - for i in drange(0, range, 0.1 * range): - for j in drange(0, range, 0.1 * range): - rk = RungeKutta(system, trajStart(i, j), tstart, h) - rk.solve(tend) - # draw the line - for tr in rk.Trajectory: - x, y = trajPlot(tr) - print(x, y) - print() - # draw the arrow - continue - l = (len(rk.Trajectory) - 1) / 3 - if l > 0 and 2 * l < len(rk.Trajectory): - p1 = rk.Trajectory[l] - p2 = rk.Trajectory[2 * l] - x1, y1 = trajPlot(p1) - x2, y2 = trajPlot(p2) - dx = -0.5 * (y2 - y1) # orthogonal to line - dy = 0.5 * (x2 - x1) # orthogonal to line - # l = math.sqrt(dx*dx + dy*dy) - # if abs(l) > 1e-3: - # l = 0.1 / l - # dx *= l - # dy *= l - print(x1 + dx, y1 + dy) - print(x2, y2) - print(x1 - dx, y1 - dy) - print() - - def singleTraj(system, trajStart, h=0.02, tend=1.0): + is_REPR_C = float("1.0000001") == float("1.0") tstart = 0.0 # compute the trajectory rk = RungeKutta(system, trajStart, tstart, h) - rk.solve(tend) + try: + rk.solve(tend) + except MemoryError: + print("SKIP") + raise SystemExit # print out trajectory for i in range(len(rk.Trajectory)): tr = rk.Trajectory[i] - print(" ".join(["{:.4f}".format(t) for t in tr])) - + tr_str = " ".join(["{:.4f}".format(t) for t in tr]) + if is_REPR_C: + # allow two small deviations for REPR_C + if tr_str == "1.0000 0.3559 0.6485 1.1944 0.9271 0.1083": + tr_str = "1.0000 0.3559 0.6485 1.1944 0.9272 0.1083" + if tr_str == "16.0000 0.3894 0.5793 0.7017 0.5686 -0.0168": + tr_str = "16.0000 0.3894 0.5793 0.7017 0.5686 -0.0167" + print(tr_str) -# phaseDiagram(sysSM, (lambda i, j: [0.354, 0.654, 1.278, 0.8 + 0.2 * i, 0.1 + 0.1 * j]), (lambda a: (a[4], a[5])), h=0.1, tend=math.log(10**17)) # initial conditions at M_Z singleTraj(sysSM, [0.354, 0.654, 1.278, 0.983, 0.131], h=0.5, tend=math.log(10**17)) # true values diff --git a/tests/misc/sys_exc_info.py b/tests/misc/sys_exc_info.py index d7e8a2d943b..c076dd572b0 100644 --- a/tests/misc/sys_exc_info.py +++ b/tests/misc/sys_exc_info.py @@ -8,13 +8,14 @@ def f(): - print(sys.exc_info()[0:2]) + e = sys.exc_info() + print(e[0], e[1]) try: raise ValueError("value", 123) except: - print(sys.exc_info()[0:2]) + print(sys.exc_info()[0], sys.exc_info()[1]) f() # Outside except block, sys.exc_info() should be back to None's diff --git a/tests/misc/sys_settrace_cov.py b/tests/misc/sys_settrace_cov.py new file mode 100644 index 00000000000..579c8a4a25e --- /dev/null +++ b/tests/misc/sys_settrace_cov.py @@ -0,0 +1,23 @@ +import sys + +try: + sys.settrace +except AttributeError: + print("SKIP") + raise SystemExit + + +def trace_tick_handler(frame, event, arg): + print("FRAME", frame) + print("LASTI", frame.f_lasti) + return None + + +def f(): + x = 3 + return x + + +sys.settrace(trace_tick_handler) +f() +sys.settrace(None) diff --git a/tests/misc/sys_settrace_cov.py.exp b/tests/misc/sys_settrace_cov.py.exp new file mode 100644 index 00000000000..423d78ec42b --- /dev/null +++ b/tests/misc/sys_settrace_cov.py.exp @@ -0,0 +1,2 @@ +FRAME +LASTI \\d\+ diff --git a/tests/misc/sys_settrace_features.py b/tests/misc/sys_settrace_features.py index 8ca6b382e3c..6eeb2b900f1 100644 --- a/tests/misc/sys_settrace_features.py +++ b/tests/misc/sys_settrace_features.py @@ -6,6 +6,10 @@ print("SKIP") raise SystemExit +if sys.version.startswith("3.12"): + # There is a CPython change in settrace that is reverted in 3.13! + print("WARNING: this test will fail when compared to CPython 3.12.x behaviour") + def print_stacktrace(frame, level=0): # Ignore CPython specific helpers. diff --git a/tests/multi_bluetooth/ble_mtu.py b/tests/multi_bluetooth/ble_mtu.py index 5f00b270cc0..92a40fbb4bc 100644 --- a/tests/multi_bluetooth/ble_mtu.py +++ b/tests/multi_bluetooth/ble_mtu.py @@ -106,6 +106,9 @@ def instance0(): # Wait for central to connect to us. conn_handle = wait_for_event(_IRQ_CENTRAL_CONNECT, TIMEOUT_MS) + # Inform the central that we have been connected to. + multitest.broadcast("peripheral is connected") + mtu = wait_for_event(_IRQ_MTU_EXCHANGED, TIMEOUT_MS) multitest.wait(f"client:discovery:{i}") @@ -138,6 +141,13 @@ def instance1(): ble.gap_connect(BDADDR[0], BDADDR[1], TIMEOUT_MS) conn_handle = wait_for_event(_IRQ_PERIPHERAL_CONNECT, TIMEOUT_MS) + # Wait for peripheral to be ready for MTU exchange. + # On ESP32 with NimBLE and IDF 5.4.2 (at least), if the MTU exchange occurs + # immediately after the peripheral connect event, the peripheral may see its + # _IRQ_MTU_EXCHANGED event before its _IRQ_CENTRAL_CONNECT event. The wait + # here ensures correct event ordering on the peripheral. + multitest.wait("peripheral is connected") + # Central-initiated mtu exchange. print("gattc_exchange_mtu") ble.gattc_exchange_mtu(conn_handle) diff --git a/tests/multi_bluetooth/ble_mtu_peripheral.py b/tests/multi_bluetooth/ble_mtu_peripheral.py index 1c0de40a0c4..50882bcd79c 100644 --- a/tests/multi_bluetooth/ble_mtu_peripheral.py +++ b/tests/multi_bluetooth/ble_mtu_peripheral.py @@ -101,6 +101,13 @@ def instance0(): # Wait for central to connect to us. conn_handle = wait_for_event(_IRQ_CENTRAL_CONNECT, TIMEOUT_MS) + # Wait for central to be ready for MTU exchange. + # On ESP32 with NimBLE and IDF 5.4.2 (at least), if the MTU exchange occurs + # immediately after the central connect event, the central may see its + # _IRQ_MTU_EXCHANGED event before its _IRQ_PERIPHERAL_CONNECT event. The + # wait here ensures correct event ordering on the central. + multitest.wait("central is connected") + # Peripheral-initiated mtu exchange. print("gattc_exchange_mtu") ble.gattc_exchange_mtu(conn_handle) @@ -142,6 +149,9 @@ def instance1(): ble.gap_connect(BDADDR[0], BDADDR[1], 5000) conn_handle = wait_for_event(_IRQ_PERIPHERAL_CONNECT, TIMEOUT_MS) + # Inform the peripheral that we have been connected to. + multitest.broadcast("central is connected") + mtu = wait_for_event(_IRQ_MTU_EXCHANGED, TIMEOUT_MS) print("gattc_discover_characteristics") diff --git a/tests/multi_bluetooth/stress_deepsleep_reconnect.py b/tests/multi_bluetooth/stress_deepsleep_reconnect.py index 7c34c036067..b588b4000b4 100644 --- a/tests/multi_bluetooth/stress_deepsleep_reconnect.py +++ b/tests/multi_bluetooth/stress_deepsleep_reconnect.py @@ -5,7 +5,9 @@ from micropython import const import time, machine, bluetooth -TIMEOUT_MS = 4000 +# Note: This value can be much lower most of the time, but an ESP32 with a boot.py +# that connects to Wi-Fi may take an extra 5 seconds after reboot. +TIMEOUT_MS = 8000 _IRQ_CENTRAL_CONNECT = const(1) _IRQ_CENTRAL_DISCONNECT = const(2) diff --git a/tests/multi_espnow/10_simple_data.py b/tests/multi_espnow/10_simple_data.py index 1d218fe9813..00d69f30d91 100644 --- a/tests/multi_espnow/10_simple_data.py +++ b/tests/multi_espnow/10_simple_data.py @@ -14,7 +14,7 @@ def init(sta_active=True, ap_active=False): - wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]] + wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]] e = espnow.ESPNow() e.active(True) e.set_pmk(default_pmk) diff --git a/tests/multi_espnow/20_send_echo.py b/tests/multi_espnow/20_send_echo.py index 4a1d1624d84..4c325bf68c5 100644 --- a/tests/multi_espnow/20_send_echo.py +++ b/tests/multi_espnow/20_send_echo.py @@ -61,7 +61,7 @@ def echo_client(e, peer, msglens): def init(sta_active=True, ap_active=False): - wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]] + wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]] e = espnow.ESPNow() e.active(True) e.set_pmk(default_pmk) diff --git a/tests/multi_espnow/30_lmk_echo.py b/tests/multi_espnow/30_lmk_echo.py index ac890804925..2a6c77c6331 100644 --- a/tests/multi_espnow/30_lmk_echo.py +++ b/tests/multi_espnow/30_lmk_echo.py @@ -92,7 +92,7 @@ def echo_client(e, peer, msglens): # Initialise the wifi and espnow hardware and software def init(sta_active=True, ap_active=False): - wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]] + wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]] e = espnow.ESPNow() e.active(True) e.set_pmk(default_pmk) diff --git a/tests/multi_espnow/40_recv_test.py b/tests/multi_espnow/40_recv_test.py index 46f4f78df48..554db16ac17 100644 --- a/tests/multi_espnow/40_recv_test.py +++ b/tests/multi_espnow/40_recv_test.py @@ -48,7 +48,7 @@ def client_send(e, peer, msg, sync): def init(sta_active=True, ap_active=False): - wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]] + wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]] e = espnow.ESPNow() e.active(True) e.set_pmk(default_pmk) diff --git a/tests/multi_espnow/50_esp32_rssi_test.py b/tests/multi_espnow/50_esp32_rssi_test.py index 6a47b540d35..8aded1c0994 100644 --- a/tests/multi_espnow/50_esp32_rssi_test.py +++ b/tests/multi_espnow/50_esp32_rssi_test.py @@ -49,7 +49,7 @@ def client_send(e, peer, msg, sync): def init(sta_active=True, ap_active=False): - wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]] + wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]] e = espnow.ESPNow() e.active(True) e.set_pmk(default_pmk) diff --git a/tests/multi_espnow/60_irq_test.py b/tests/multi_espnow/60_irq_test.py index 37fc57ce4ae..db8b8168690 100644 --- a/tests/multi_espnow/60_irq_test.py +++ b/tests/multi_espnow/60_irq_test.py @@ -49,7 +49,7 @@ def client_send(e, peer, msg, sync): def init(sta_active=True, ap_active=False): - wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]] + wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]] e = espnow.ESPNow() e.active(True) e.set_pmk(default_pmk) diff --git a/tests/multi_espnow/70_channel.py b/tests/multi_espnow/70_channel.py new file mode 100644 index 00000000000..f3e8b947b12 --- /dev/null +++ b/tests/multi_espnow/70_channel.py @@ -0,0 +1,89 @@ +# Test that ESP-NOW picks up the channel configuration for STA +# mode on ESP32. +# +# Note that setting the channel on a peer in ESP-NOW on modern ESP-IDF only +# checks it against the configured channel, it doesn't ever change the radio +# channel +import sys +import time + +try: + import network + import espnow +except ImportError: + print("SKIP") + raise SystemExit + +# ESP8266 doesn't support config('channel') on the STA interface, +# and the channel parameter to add_peer doesn't appear to set the +# channel either. +if sys.platform == "esp8266": + print("SKIP") + raise SystemExit + + +timeout_ms = 1000 +default_pmk = b"MicroPyth0nRules" + +CHANNEL = 3 +WRONG_CHANNEL = 8 + + +def init_sta(): + sta = network.WLAN(network.WLAN.IF_STA) + e = espnow.ESPNow() + e.active(True) + sta.active(True) + sta.disconnect() # Force AP disconnect for any saved config, important so the channel doesn't change + sta.config(channel=CHANNEL) + e.set_pmk(default_pmk) + return sta, e + + +# Receiver +def instance0(): + sta, e = init_sta() + multitest.globals(PEER=sta.config("mac")) + multitest.next() + print(sta.config("channel")) + while True: + peer, msg = e.recv(timeout_ms) + if peer is None: + print("Timeout") + break + print(msg) + e.active(False) + + +# Sender +def instance1(): + sta, e = init_sta() + multitest.next() + peer = PEER + + # both instances set channel via sta.config(), above + msg = b"sent to right channel 1" + e.add_peer(peer, channel=CHANNEL) + for _ in range(3): + e.send(peer, msg) + e.del_peer(peer) + print(sta.config("channel")) + + sta.config(channel=WRONG_CHANNEL) + msg = b"sent to wrong channel" + e.add_peer(peer, channel=WRONG_CHANNEL) + for _ in range(3): + e.send(peer, msg) + e.del_peer(peer) + print(sta.config("channel")) + + # switching back to the correct channel should also work + sta.config(channel=CHANNEL) + msg = b"sent to right channel 2" + e.add_peer(peer, channel=CHANNEL) + for _ in range(3): + e.send(peer, msg) + e.del_peer(peer) + print(sta.config("channel")) + + e.active(False) diff --git a/tests/multi_espnow/70_channel.py.exp b/tests/multi_espnow/70_channel.py.exp new file mode 100644 index 00000000000..d8553966032 --- /dev/null +++ b/tests/multi_espnow/70_channel.py.exp @@ -0,0 +1,13 @@ +--- instance0 --- +3 +b'sent to right channel 1' +b'sent to right channel 1' +b'sent to right channel 1' +b'sent to right channel 2' +b'sent to right channel 2' +b'sent to right channel 2' +Timeout +--- instance1 --- +3 +8 +3 diff --git a/tests/multi_espnow/75_rate.py b/tests/multi_espnow/75_rate.py new file mode 100644 index 00000000000..9c05a24bd00 --- /dev/null +++ b/tests/multi_espnow/75_rate.py @@ -0,0 +1,92 @@ +# Test configuring transmit rates for ESP-NOW on ESP32 + +import sys +import time + +try: + import network + import espnow +except ImportError: + print("SKIP") + raise SystemExit + +# ESP8266 doesn't support multiple ESP-NOW data rates +if sys.platform == "esp8266": + print("SKIP") + raise SystemExit + +# Currently the config(rate=...) implementation is not compatible with ESP32-C6 +# (this test passes when C6 is receiver, but not if C6 is sender.) +if "ESP32C6" in sys.implementation._machine: + print("SKIP") + raise SystemExit + +# ESP32-C2 doesn't support Long Range mode. This test is currently written assuming +# LR mode can be enabled. +if "ESP32C2" in sys.implementation._machine: + print("SKIP") + raise SystemExit + + +timeout_ms = 1000 +default_pmk = b"MicroPyth0nRules" +CHANNEL = 9 + + +def init_sta(): + sta = network.WLAN(network.WLAN.IF_STA) + e = espnow.ESPNow() + e.active(True) + sta.active(True) + sta.disconnect() # Force AP disconnect for any saved config, important so the channel doesn't change + sta.config(channel=CHANNEL) + e.set_pmk(default_pmk) + # Enable both default 802.11 modes and Long Range modes + sta.config(protocol=network.WLAN.PROTOCOL_LR | network.WLAN.PROTOCOL_DEFAULT) + return sta, e + + +# Receiver +def instance0(): + sta, e = init_sta() + multitest.globals(PEER=sta.config("mac")) + multitest.next() + while True: + peer, msg = e.recv(timeout_ms) + if peer is None: + print("Timeout") + break + # Note that we don't have any way in Python to tell what data rate this message + # was received with, so we're assuming the rate was correct. + print(msg) + e.active(False) + + +# Sender +def instance1(): + sta, e = init_sta() + multitest.next() + peer = PEER + + e.add_peer(peer) + # Test normal, non-LR rates + for msg, rate in ( + (b"default rate", None), + (b"5Mbit", espnow.RATE_5M), + (b"11Mbit", espnow.RATE_11M), + (b"24Mbit", espnow.RATE_24M), + (b"54Mbit", espnow.RATE_54M), + (b"250K LR", espnow.RATE_LORA_250K), + (b"500K LR", espnow.RATE_LORA_500K), + # switch back to non-LR rates to check it's all OK + (b"1Mbit again", espnow.RATE_1M), + (b"11Mbit again", espnow.RATE_11M), + ): + if rate is not None: + e.config(rate=rate) + for _ in range(3): + e.send(peer, msg) + time.sleep_ms(50) # give messages some time to be received before continuing + e.del_peer(peer) + + e.active(False) diff --git a/tests/multi_espnow/75_rate.py.exp b/tests/multi_espnow/75_rate.py.exp new file mode 100644 index 00000000000..5a275ee6b28 --- /dev/null +++ b/tests/multi_espnow/75_rate.py.exp @@ -0,0 +1,31 @@ +--- instance0 --- +b'default rate' +b'default rate' +b'default rate' +b'5Mbit' +b'5Mbit' +b'5Mbit' +b'11Mbit' +b'11Mbit' +b'11Mbit' +b'24Mbit' +b'24Mbit' +b'24Mbit' +b'54Mbit' +b'54Mbit' +b'54Mbit' +b'250K LR' +b'250K LR' +b'250K LR' +b'500K LR' +b'500K LR' +b'500K LR' +b'1Mbit again' +b'1Mbit again' +b'1Mbit again' +b'11Mbit again' +b'11Mbit again' +b'11Mbit again' +Timeout +--- instance1 --- + diff --git a/tests/multi_espnow/80_asyncio_client.py b/tests/multi_espnow/80_asyncio_client.py index 4d58a451c1c..8c34b98a3c4 100644 --- a/tests/multi_espnow/80_asyncio_client.py +++ b/tests/multi_espnow/80_asyncio_client.py @@ -50,7 +50,7 @@ def client_send(e, peer, msg, sync): def init(e, sta_active=True, ap_active=False): - wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]] + wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]] e.active(True) e.set_pmk(default_pmk) wlans[0].active(sta_active) diff --git a/tests/multi_espnow/81_asyncio_server.py b/tests/multi_espnow/81_asyncio_server.py index df1dbb2ac8a..e6002582c08 100644 --- a/tests/multi_espnow/81_asyncio_server.py +++ b/tests/multi_espnow/81_asyncio_server.py @@ -30,7 +30,7 @@ def client_send(e, peer, msg, sync): def init(e, sta_active=True, ap_active=False): - wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]] + wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]] e.active(True) e.set_pmk(default_pmk) wlans[0].active(sta_active) diff --git a/tests/multi_espnow/90_memory_test.py b/tests/multi_espnow/90_memory_test.py index 5e80eb0fdf6..b59ff61b594 100644 --- a/tests/multi_espnow/90_memory_test.py +++ b/tests/multi_espnow/90_memory_test.py @@ -65,7 +65,7 @@ def echo_client(e, peer, msglens): def init(sta_active=True, ap_active=False): - wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]] + wlans = [network.WLAN(i) for i in [network.WLAN.IF_STA, network.WLAN.IF_AP]] e = espnow.ESPNow() e.active(True) e.set_pmk(default_pmk) diff --git a/tests/multi_extmod/machine_can_01_rxtx_simple.py b/tests/multi_extmod/machine_can_01_rxtx_simple.py new file mode 100644 index 00000000000..fbc7692926d --- /dev/null +++ b/tests/multi_extmod/machine_can_01_rxtx_simple.py @@ -0,0 +1,35 @@ +from machine import CAN +import time + +ID_0 = 0x50 +ID_1 = 0x50333 +FLAGS_1 = CAN.FLAG_EXT_ID # ID_1 is an extended CAN ID + +can = CAN(1, 500_000) +can.set_filters(None) # receive all + + +def print_rx(can_id, data, flags, errors): + print(hex(can_id), bytes(data), hex(flags), hex(errors)) + + +def instance0(): + multitest.next() + + # receive from instance1 first + while not (rx := can.recv()): + time.sleep(0) + print_rx(*rx) + + # now send one + can.send(ID_0, b"1234") + + +def instance1(): + multitest.next() + + can.send(ID_1, b"ABCD", FLAGS_1) + + while not (rx := can.recv()): + time.sleep(0) + print_rx(*rx) diff --git a/tests/multi_extmod/machine_can_01_rxtx_simple.py.exp b/tests/multi_extmod/machine_can_01_rxtx_simple.py.exp new file mode 100644 index 00000000000..479b6809490 --- /dev/null +++ b/tests/multi_extmod/machine_can_01_rxtx_simple.py.exp @@ -0,0 +1,4 @@ +--- instance0 --- +0x50333 b'ABCD' 0x2 0x0 +--- instance1 --- +0x50 b'1234' 0x0 0x0 diff --git a/tests/multi_extmod/machine_can_02_rx_callback.py b/tests/multi_extmod/machine_can_02_rx_callback.py new file mode 100644 index 00000000000..80b13dcd622 --- /dev/null +++ b/tests/multi_extmod/machine_can_02_rx_callback.py @@ -0,0 +1,122 @@ +from machine import CAN +import time + +# Test the CAN.IRQ_RX irq handler, including overflow + +rx_overflow = False +rx_full = False +received = [] + +# CAN IDs +ID_SPAM = 0x345 # messages spammed into the receive FIFO +ID_ACK_OFLOW = 0x055 # message the receiver sends after it's seen an overflow +ID_AFTER = 0x100 # message the sender sends after the ACK + +can = CAN(1, 500_000) + + +# A very basic "soft" receiver handler that stores received messages into a global list +def receiver_irq_recv(can): + global rx_overflow, rx_full + + assert can.irq().flags() & can.IRQ_RX # the only enabled IRQ + + can_id, data, _flags, errors = can.recv() + + received.append((can_id, None)) + + # The FIFO is expected not to overflow by itself, wait until 40 messages + # have been received and then block the receive handler to induce an overflow + if len(received) == 40: + assert not rx_overflow # shouldn't have already happened, either + time.sleep_ms(500) + + if not rx_overflow and (errors & CAN.RECV_ERR_OVERRUN): + # expected this should happen on the very next message after + # the one where we slept for 500ms + print("irq_recv overrun", len(received)) + received.clear() # check we still get some messages, see rx_spam print line below + rx_overflow = True + + # also expect the FIFO to be FULL again immediately after overrunning and rx_overflow event + if rx_overflow and (errors & CAN.RECV_ERR_OVERRUN | CAN.RECV_ERR_FULL) == CAN.RECV_ERR_FULL: + rx_full = True + + +# Receiver +def instance0(): + can.irq(receiver_irq_recv, trigger=can.IRQ_RX, hard=False) + + can.set_filters(None) # receive all + + multitest.next() + + while not rx_overflow: + pass # Resume ASAP after FIFO0 overflows + + can.send(ID_ACK_OFLOW, b"overflow") + + # at least one ID_SPAM message should have been received + # *after* we overflowed and 'received' was clear in the irq handler + print("rx_spam", any(r[0] == ID_SPAM for r in received)) + + # wait until the "after" message is received + for n in range(100): + if any(r[0] == ID_AFTER for r in received): + break + time.sleep_ms(10) + + can.irq(None) # disable the IRQ + received.clear() + + # at some point while waiting for ID_AFTER the FIFO should have gotten + # full again + print("rx_full", rx_full) + + # now IRQ is disabled, no new messages should be received + time.sleep_ms(250) + print("len", len(received)) + + +received_ack = False + +# reusing the result buffer so sender_irq_recv can be 'hard' +sender_irq_result = [None, memoryview(bytearray(64)), None, None] + + +def sender_irq_recv(can): + global received_ack + + assert can.irq().flags() & can.IRQ_RX # the only enabled IRQ + + can_id, data, _flags, _errors = can.recv(sender_irq_result) + print("sender_irq_recv", can_id, len(data)) # should be ID_ACK_OFLOW and "overflow" payload + received_ack = True + + +# Sender +def instance1(): + can.irq(sender_irq_recv, CAN.IRQ_RX, hard=True) + + can.set_filters(None) + + multitest.next() + + # Spam out messages until the receiver tells us its RX FIFO is full. + # + # The RX FIFO on the receiver can vary from 3 deep (BXCAN) to 25 deep (STM32H7), + # so we keep sending to it until we see a CAN message on ID_ACK_OFLOW indicating + # the receiver's FIFO has overflowed + while not received_ack: + for i in range(255): + while can.send(ID_SPAM, bytes([i] * 8)) is None and not received_ack: + # Don't overflow the TX FIFO + time.sleep_ms(1) + if received_ack: + break + + # give the receiver some time to make space in the FIFO + time.sleep_ms(200) + + # send the final message, the receiver should get this one + can.send(ID_AFTER, b"aaaaa") diff --git a/tests/multi_extmod/machine_can_02_rx_callback.py.exp b/tests/multi_extmod/machine_can_02_rx_callback.py.exp new file mode 100644 index 00000000000..8e4e5dc6475 --- /dev/null +++ b/tests/multi_extmod/machine_can_02_rx_callback.py.exp @@ -0,0 +1,7 @@ +--- instance0 --- +irq_recv overrun 41 +rx_spam True +rx_full True +len 0 +--- instance1 --- +sender_irq_recv 85 8 diff --git a/tests/multi_extmod/machine_can_03_rx_filters.py b/tests/multi_extmod/machine_can_03_rx_filters.py new file mode 100644 index 00000000000..a56acbbe91e --- /dev/null +++ b/tests/multi_extmod/machine_can_03_rx_filters.py @@ -0,0 +1,103 @@ +from machine import CAN +import time + +# Test for filtering capabilities + +can = CAN(1, 500_000) + +# IDs and filter phases used for the 'single id' part of the test +SINGLE_EXT_ID = (0x1234_5678, 0x1FFF_FFFF, CAN.FLAG_EXT_ID) +SINGLE_STD_ID = (0x505, 0x7FF, 0) +SINGLE_ID_PHASES = [ + ("single ext id", [SINGLE_EXT_ID]), + ("single std id", [SINGLE_STD_ID]), + ("ext+std ids", [SINGLE_EXT_ID, SINGLE_STD_ID]), + ("std+ext ids", [SINGLE_STD_ID, SINGLE_EXT_ID]), # these two should be equivalent + ("accept none", []), + ("accept all", None), + ("accept none again", ()), +] + + +# Receiver +def receiver_irq_recv(can): + assert can.irq().flags() & can.IRQ_RX # the only enabled IRQ + can_id, data, _flags, _errors = can.recv() + print("recv", hex(can_id), data.hex()) + + +def instance0(): + can.irq(receiver_irq_recv, trigger=can.IRQ_RX, hard=False) + + multitest.next() + + # Configure to receive standard frames (in a range), and + # extended frames (in a range). + can.set_filters([(0x300, 0x300, 0), (0x3000, 0x3000, CAN.FLAG_EXT_ID)]) + multitest.broadcast("ready id ranges") + + # Run through the phases of filtering for individual IDs + for phase, filters in SINGLE_ID_PHASES: + multitest.wait("configure " + phase) + if filters and len(filters) > CAN.FILTERS_MAX: + # this check really exists to add test coverage for the FILTERS_MAX constant + print("Warning: Too many filters for hardware!") + can.set_filters(filters) + print("receiver configured " + phase) + multitest.broadcast("ready " + phase) + + multitest.wait("Sender done") + + +def send_messages(messages): + for can_id, payload, flags in messages: + r = can.send(can_id, payload, flags) + if r is None: + print("Failed to send:", hex(can_id), payload.hex()) + time.sleep_ms(5) # avoid flooding either our or the receiver's FIFO + + +# Sender +def instance1(): + multitest.next() + multitest.wait("ready id ranges") + + print("Sending ID ranges...") + for i in range(3): + send_messages( + [ + (0x345, bytes([i, 0xFF] * (i + 1)), 0), + (0x3700 + i, bytes([0xEE] * (i + 1)), CAN.FLAG_EXT_ID), + (0x123, b"abcdef", 0), # matches no filter, expect ACKed but not received + ] + ) + + # Now move on to single ID filtering + + single_id_messages = [ + (0x1234_5678, b"\x01\x02\x03\x04\x05", CAN.FLAG_EXT_ID), # matches ext id + (0x0234_5678, b"\x00\x00", CAN.FLAG_EXT_ID), # no match + (0x678, b"\x00\x01", 0), # no match + (0x505, b"\x06\x07\x08\x09\x0a\x0b", 0), # matches standard id + (0x345, b"\x00\x02", 0), # no match (in prev filter) + (0x1234_5679, b"\x00\x03", CAN.FLAG_EXT_ID), # no match + (0x3705, b"\x00\x04", CAN.FLAG_EXT_ID), # no match (in prev filter) + (0x1234_5678, b"\x01\x02\x03", CAN.FLAG_EXT_ID), # matches ext id + (0x505, b"\x04\x05\x06", 0), # matches standard id + (0x505, b"\x00\x05", CAN.FLAG_EXT_ID), # no match (is ext id) + (0x507, b"\x00\x06", 0), # no match + (0x1334_5678, b"\x00\x07", CAN.FLAG_EXT_ID), # no match + (0x1234_5670, b"\x00\x08", CAN.FLAG_EXT_ID), # no match + ] + + # Send the same list of messages for each phase of the test. + # The receiver will have configured different filters, and the .exp + # file is what selects which messages should be received or not. + for phase, _ in SINGLE_ID_PHASES: + multitest.broadcast("configure " + phase) + multitest.wait("ready " + phase) + print("Sending for " + phase + "...") + send_messages(single_id_messages) + + print("Sender done") + multitest.broadcast("Sender done") diff --git a/tests/multi_extmod/machine_can_03_rx_filters.py.exp b/tests/multi_extmod/machine_can_03_rx_filters.py.exp new file mode 100644 index 00000000000..d55c0c97d15 --- /dev/null +++ b/tests/multi_extmod/machine_can_03_rx_filters.py.exp @@ -0,0 +1,49 @@ +--- instance0 --- +recv 0x345 00ff +recv 0x3700 ee +recv 0x345 01ff01ff +recv 0x3701 eeee +recv 0x345 02ff02ff02ff +recv 0x3702 eeeeee +receiver configured single ext id +recv 0x12345678 0102030405 +recv 0x12345678 010203 +receiver configured single std id +recv 0x505 060708090a0b +recv 0x505 040506 +receiver configured ext+std ids +recv 0x12345678 0102030405 +recv 0x505 060708090a0b +recv 0x12345678 010203 +recv 0x505 040506 +receiver configured std+ext ids +recv 0x12345678 0102030405 +recv 0x505 060708090a0b +recv 0x12345678 010203 +recv 0x505 040506 +receiver configured accept none +receiver configured accept all +recv 0x12345678 0102030405 +recv 0x2345678 0000 +recv 0x678 0001 +recv 0x505 060708090a0b +recv 0x345 0002 +recv 0x12345679 0003 +recv 0x3705 0004 +recv 0x12345678 010203 +recv 0x505 040506 +recv 0x505 0005 +recv 0x507 0006 +recv 0x13345678 0007 +recv 0x12345670 0008 +receiver configured accept none again +--- instance1 --- +Sending ID ranges... +Sending for single ext id... +Sending for single std id... +Sending for ext+std ids... +Sending for std+ext ids... +Sending for accept none... +Sending for accept all... +Sending for accept none again... +Sender done diff --git a/tests/multi_extmod/machine_can_04_tx_order.py b/tests/multi_extmod/machine_can_04_tx_order.py new file mode 100644 index 00000000000..204bdafd59d --- /dev/null +++ b/tests/multi_extmod/machine_can_04_tx_order.py @@ -0,0 +1,170 @@ +from machine import CAN +import time +from random import seed, randrange + +import micropython + +micropython.alloc_emergency_exception_buf(256) +seed(0) + +# Testing that transmit order obeys the priority ordering + +ID_LOW = 0x500 +ID_HIGH = 0x200 + +NUM_MSGS = 255 + +MSG_LEN = 4 + +can = CAN(1, 500_000) + + +def check_sequence(items, label): + # The full range of NUM_MSGS values should have been received or sent, in + # order, without duplicates + if len(items) != NUM_MSGS: + print(label, "wrong count", len(items), "vs", NUM_MSGS) + if items == list(range(NUM_MSGS)): + print(label, "OK") + else: + print(label, "error:") + print(items) + + +# Receiver + +# lists of received messages, one list per ID +received_low = [] +received_high = [] + + +def irq_recv(can): + while can.irq().flags() & can.IRQ_RX: + can_id, data, flags, _errors = can.recv() + + if can_id == ID_LOW and len(data) == MSG_LEN: + received_low.append(data[0]) + elif can_id == ID_HIGH and len(data) == MSG_LEN: + received_high.append(data[0]) + else: + print("unexpected recv", can_id, data, flags) + + +def instance0(): + can.irq(irq_recv, trigger=can.IRQ_RX, hard=False) + can.set_filters(None) # receive all + + multitest.next() + + multitest.wait("sender done") + check_sequence(received_low, "Low prio received") + check_sequence(received_high, "High prio received") + + +# Sender + +## Messages pending to send +pending_low = list(range(NUM_MSGS)) +pending_high = list(range(NUM_MSGS)) + +# List of the messages currently queued to send +tx_queue = [None] * CAN.TX_QUEUE_LEN + +# Messages sent, recorded in order as [high_prio, val] +sent = [] +for _ in range(NUM_MSGS * 2): + sent.append([None, None]) +num_sent = 0 + + +def irq_send(can): + global num_sent + + while flags := can.irq().flags(): + assert flags & can.IRQ_TX # the only enabled IRQ + + idx = (flags >> can.IRQ_TX_IDX_SHIFT) & can.IRQ_TX_IDX_MASK + success = not (flags & can.IRQ_TX_FAILED) + + if not success: + return # We don't worry about failures here + + if not tx_queue[idx]: + print("bad done", idx, success) + return + + was_high, val = tx_queue[idx] + tx_queue[idx] = None + sent[num_sent][0] = was_high + sent[num_sent][1] = val + num_sent += 1 + + +def instance1(): + # note: this test can pass with hard=True, but in a debug build + # the completion IRQ may race ahead of setting tx_queue[idx], below + can.irq(irq_send, trigger=can.IRQ_TX, hard=False) + data = bytearray(MSG_LEN) + + multitest.next() + + while pending_low or pending_high: + if pending_high: + val = pending_high.pop(0) + data[0] = val + data[1] = 1 + while True: + idx = can.send(ID_HIGH, data) + if idx is None: + continue # keep trying until a queue spot opens up + old = tx_queue[idx] + tx_queue[idx] = (True, val) + if old: + print("error high priority queue race", idx, val, old) + break + + for _ in range(randrange(4)): + # Try and queue many low priority messages (expecting most will fail) + if pending_low: + val = pending_low[0] + data[0] = val + data[1] = 0 + idx = can.send(ID_LOW, data) + if idx is None: + # don't retry indefinitely for low priority messages + continue + + old = tx_queue[idx] + tx_queue[idx] = (False, val) + pending_low.pop(0) + if old is not None: + print("error low priority queue race", idx, val, old) + + print("waiting for tx queue to empty...") + while any(x is not None for x in tx_queue): + pass + + multitest.broadcast("sender done") + + # Check we sent the right number of messages + if num_sent != 2 * NUM_MSGS: + print("Sent %d expected %d" % (num_sent, 2 * NUM_MSGS)) + else: + print("Sent right number of messages") + + # Check the low and high priority messages all arrived in order + sent_low = [val for (prio, val) in sent[:num_sent] if prio == False] + sent_high = [val for (prio, val) in sent[:num_sent] if prio == True] + check_sequence(sent_low, "Low prio sent") + check_sequence(sent_high, "High prio sent") + + # check that high priority message queue items always stayed ahead of the low priority + high_val = -1 + for idx, (prio, val) in enumerate(sent): + if prio: + high_val = val + elif high_val <= val and val < NUM_MSGS - 1: + print( + "Low priority message %d overtook high priority %d at index %d" + % (val, high_val, idx) + ) diff --git a/tests/multi_extmod/machine_can_04_tx_order.py.exp b/tests/multi_extmod/machine_can_04_tx_order.py.exp new file mode 100644 index 00000000000..a2de6ee2706 --- /dev/null +++ b/tests/multi_extmod/machine_can_04_tx_order.py.exp @@ -0,0 +1,8 @@ +--- instance0 --- +Low prio received OK +High prio received OK +--- instance1 --- +waiting for tx queue to empty... +Sent right number of messages +Low prio sent OK +High prio sent OK diff --git a/tests/multi_extmod/machine_can_05_tx_prio_cancel.py b/tests/multi_extmod/machine_can_05_tx_prio_cancel.py new file mode 100644 index 00000000000..64756a1a1af --- /dev/null +++ b/tests/multi_extmod/machine_can_05_tx_prio_cancel.py @@ -0,0 +1,118 @@ +from machine import CAN +import time + +# Check that cancelling a low priority outgoing message and replacing it with a +# high priority message causes it to be transmitted successfully onto a busy bus + +recv = [] + +ITERS = 5 + +can = CAN(1, 500_000) + + +def irq_recv(can): + global recv_std_id + while can.irq().flags() & can.IRQ_RX: + can_id, data, flags, _errors = can.recv() + assert flags & CAN.FLAG_EXT_ID # test uses all extended IDs + if len(recv) < ITERS: + recv.append(can_id) + + +def instance0(): + can.irq(irq_recv, trigger=can.IRQ_RX, hard=False) + can.set_filters(None) # receive all + + multitest.next() + + # "Babble" medium priority messages onto the bus to prevent + # instance1() from sending anything lower priority than this + while len(recv) < ITERS: + for id in range(0x5000, 0x6000): + can.send(id, b"BABBLE", CAN.FLAG_EXT_ID) + if len(recv) >= ITERS: + break + + print("received", ITERS, "messages") + for can_id in recv: + print(hex(can_id)) # should be the high priority messages from instance1, only + + multitest.wait("sender done") + print("done") + + +last_idx = 0 +total_cancels = 0 +total_sent = 0 + + +def irq_send(can): + global total_cancels, total_sent + + while flags := can.irq().flags(): + assert flags & can.IRQ_TX # the only enabled IRQ + + idx = (flags >> can.IRQ_TX_IDX_SHIFT) & can.IRQ_TX_IDX_MASK + + if flags & can.IRQ_TX_FAILED: + # we should only see failed transmits due to cancels in buffer 'last_idx' + assert idx == last_idx + total_cancels += 1 + else: + # this includes the messages we explicitly send, plus queued low + # priority messages once the receiver stops 'babbling' on the bus + total_sent += 1 + + +def instance1(): + global last_idx + can.irq(irq_send, trigger=can.IRQ_TX, hard=True) + + multitest.next() + + for i in range(ITERS): + # Fill the transmit queue with low priority messages (all extended IDs) + last_idx = 0 + if i < 3: + # For the first 3 iterations, send unique message IDs + id_range = range(0x7000, 0x7FFF) + flags = CAN.FLAG_EXT_ID + else: + # For the last iterations, repeat the same ID but tell controller to ignore + # ordering (allows it to queue more than one despite hardware limitations) + id_range = [0x50000 + i] * CAN.TX_QUEUE_LEN + flags = CAN.FLAG_EXT_ID | CAN.FLAG_UNORDERED + + for id in id_range: + idx = can.send(id, b"LOWPRIO", flags) + if idx is None: + break # send queue is full, stop trying to send + last_idx = idx + + time.sleep_ms(50) # the send queue shouldn't empty as instance0 is "babbling" + + # try and cancel the last message we queued + res = can.cancel_send(last_idx) + print(i, "cancel result", res) + + # send a high priority message, that we expect to go out + idx = can.send(0x500 + i, b"HIPRIO", CAN.FLAG_EXT_ID) + print(i, "send result", idx is not None) + + # make sure this message is sent onto the bus + time.sleep_ms(1) + + multitest.broadcast("sender done") + + # let the entire transmit queue drain, now instance0 should have gone quiet + time.sleep_ms(50) + + print("total cancels", total_cancels) # should equal ITERS + + if total_sent == CAN.TX_QUEUE_LEN - 1 + ITERS: + # expect we send one message for each of ITERS, plus all low priority + # queued messages once instance0 stops babbling on the bus + print("total sent OK") + else: + print("total sent", total_sent, CAN.TX_QUEUE_LEN - 1 + ITERS) diff --git a/tests/multi_extmod/machine_can_05_tx_prio_cancel.py.exp b/tests/multi_extmod/machine_can_05_tx_prio_cancel.py.exp new file mode 100644 index 00000000000..26bab097c44 --- /dev/null +++ b/tests/multi_extmod/machine_can_05_tx_prio_cancel.py.exp @@ -0,0 +1,21 @@ +--- instance0 --- +received 5 messages +0x500 +0x501 +0x502 +0x503 +0x504 +done +--- instance1 --- +0 cancel result True +0 send result True +1 cancel result True +1 send result True +2 cancel result True +2 send result True +3 cancel result True +3 send result True +4 cancel result True +4 send result True +total cancels 5 +total sent OK diff --git a/tests/multi_extmod/machine_can_06_remote_req.py b/tests/multi_extmod/machine_can_06_remote_req.py new file mode 100644 index 00000000000..4a1414c0946 --- /dev/null +++ b/tests/multi_extmod/machine_can_06_remote_req.py @@ -0,0 +1,70 @@ +from machine import CAN +import time + +# Test CAN remote transmission requests + +ID = 0x750 + +FILTER_ID = 0x101 +FILTER_MASK = 0x7FF + +can = CAN(1, 500_000) + + +def receiver_irq_recv(can): + assert can.irq().flags() & can.IRQ_RX # the only enabled IRQ + can_id, data, flags, _errors = can.recv() + is_rtr = flags == CAN.FLAG_RTR + print( + "recv", + hex(can_id), + is_rtr, + len(data) if is_rtr else bytes(data), + ) + if is_rtr: + # The 'data' response of a remote request should be all zeroes + assert bytes(data) == b"\x00" * len(data) + + +# Receiver +def instance0(): + can.irq(receiver_irq_recv, trigger=can.IRQ_RX, hard=False) + can.set_filters(None) # receive all + + multitest.next() + + multitest.wait("enable filter") + can.set_filters([(FILTER_ID, FILTER_MASK, 0)]) + multitest.broadcast("filter set") + + multitest.wait("done") + + +# Sender +def instance1(): + multitest.next() + + can.send(ID, b"abc", CAN.FLAG_RTR) # length==3 remote request + time.sleep_ms(5) + can.send(ID, b"abc", 0) # regular message using the same ID + time.sleep_ms(5) + can.send(ID, b"abcde", CAN.FLAG_RTR) # length==5 remote request + time.sleep_ms(5) + + multitest.broadcast("enable filter") + multitest.wait("filter set") + + # these two messages should be filtered out + can.send(ID, b"abc", CAN.FLAG_RTR) # length==3 remote request + time.sleep_ms(5) + can.send(ID, b"abc", 0) # regular message using the same ID + time.sleep_ms(5) + + # these messages should be filtered in + can.send(FILTER_ID, b"def", CAN.FLAG_RTR) # length==3 remote request + time.sleep_ms(5) + can.send(FILTER_ID, b"hij", 0) # regular message using the same ID + time.sleep_ms(5) + + multitest.broadcast("done") + print("done") diff --git a/tests/multi_extmod/machine_can_06_remote_req.py.exp b/tests/multi_extmod/machine_can_06_remote_req.py.exp new file mode 100644 index 00000000000..fe94508ba3d --- /dev/null +++ b/tests/multi_extmod/machine_can_06_remote_req.py.exp @@ -0,0 +1,8 @@ +--- instance0 --- +recv 0x750 True 3 +recv 0x750 False b'abc' +recv 0x750 True 5 +recv 0x101 True 3 +recv 0x101 False b'hij' +--- instance1 --- +done diff --git a/tests/multi_extmod/machine_can_07_error_states.py b/tests/multi_extmod/machine_can_07_error_states.py new file mode 100644 index 00000000000..e7eec513ee1 --- /dev/null +++ b/tests/multi_extmod/machine_can_07_error_states.py @@ -0,0 +1,205 @@ +from machine import CAN +from micropython import const +import time + +# Test that without a second CAN node on the network the controller will +# correctly go into the correct error states, and can then recover. +# +# Note this test depends on no other CAN node being active on the network apart +# from the two test instances. (Although it's OK for extra nodes to be in a +# "listen only" mode where they won't ACK messages.) + +rx_overflow = False +rx_full = False +received = [] + +# CAN IDs +_ID = const(0x100) + +# can.state() result list indexes, as constants +_IDX_TEC = const(0) +_IDX_REC = const(1) +_IDX_NUM_WARNING = const(2) +_IDX_NUM_PASSIVE = const(3) +_IDX_NUM_BUS_OFF = const(4) +_IDX_PEND_TX = const(5) + +can = CAN(1, 500_000) + + +def state_name(state): + for name in dir(CAN): + if name.startswith("STATE_") and state == getattr(CAN, name): + return name + return f"UNKNOWN-{state}" + + +def irq_recv(can): + while can.irq().flags() & can.IRQ_RX: + can_id, data, _flags, errors = can.recv() + print("recv", hex(can_id), data.hex()) + + +# Receiver +def instance0(): + can.irq(irq_recv, trigger=can.IRQ_RX, hard=False) + + can.set_filters(None) # receive all + + multitest.next() + + # Receive at least one CAN message before sender asks us to disable the controller + + multitest.wait("disable receiver") + can.deinit() + print("can deinit()") + multitest.broadcast("receiver disabled") + + # Wait for the sender to tell us to re-enable + + multitest.wait("enable receiver") + # note the irq is no longer active after deinit() + can.init(500_000) + print("can init()") + multitest.broadcast("receiver enabled") + + # Receive CAN messages until the sender asks us to switch to an invalid baud rate + + multitest.wait("switch baud") + can.init(125_000) + print("can switch baud") + multitest.broadcast("switched baud") + + time.sleep_ms(1) + + print("sending bad msg") + # trying to send this frame should introduce more bus errors + idx_bad = can.send(_ID, b"BADBAUD") + + multitest.wait("fix baud") + print("sending cancelling") + print("cancelled", can.cancel_send(idx_bad)) + print("re-init") + can.init(500_000) + multitest.broadcast("fixed baud") + + # Should be receiving CAN messages OK again + + print("done") + + +def irq_sender(can): + while flags := can.irq().flags(): + if flags & can.IRQ_STATE: + print("irq state", can.state()) + if flags & can.IRQ_TX: + print("irq sent", not (flags & can.IRQ_TX_FAILED)) + + +# Sender +def instance1(): + can.irq(irq_sender, CAN.IRQ_STATE | CAN.IRQ_TX, hard=False) + + can.set_filters(None) + + multitest.next() + + print("started", state_name(can.state())) # should be ERROR_ACTIVE + + # Send a single message to the receiver, to verify it's working + can.send(_ID, b"PAYLOAD") + active_counters = can.get_counters() + # print(active_counters) # DEBUG + + multitest.broadcast("disable receiver") + multitest.wait("receiver disabled") + + # Now the receiver shouldn't be ACKing our frames, queue will stay full + # ... we will get the ISR for ERROR_WARNING but it'll go from ERROR_WARNING to ERROR_PASSIVE + # very quickly as all messages are failing + can.send(_ID, b"MORE") + while can.state() in (CAN.STATE_ACTIVE, CAN.STATE_WARNING): + pass + + print(state_name(can.state())) # should be ERROR_PASSIVE now + passive_counters = can.get_counters() + # print(passive_counters) # DEBUG + print("tec increased", passive_counters[_IDX_TEC] > active_counters[_IDX_TEC]) + print("tec over thresh", passive_counters[_IDX_TEC] >= 128) + # we should have counted exactly one ERROR_WARNING and ERROR_PASSIVE transition + print( + "counted warning", + passive_counters[_IDX_NUM_WARNING] == active_counters[_IDX_NUM_WARNING] + 1, + ) + print( + "counted passive", + passive_counters[_IDX_NUM_PASSIVE] == active_counters[_IDX_NUM_PASSIVE] + 1, + ) + print("some pending tx", passive_counters[_IDX_PEND_TX] > 0) + + # Re-enable the receiver which should allow us to go from ERROR_PASSIVE to ERROR_WARNING + multitest.broadcast("enable receiver") + multitest.wait("receiver enabled") + + can.send(_ID, b"MORE") + while can.state() == CAN.STATE_PASSIVE: + pass + + print(state_name(can.state())) # should be ERROR_WARNING now + warning_counters = can.get_counters() + # print(warning_counters) # DEBUG + print("tec decreased", warning_counters[_IDX_TEC] < passive_counters[_IDX_TEC]) + print( + "tec below thresh", warning_counters[_IDX_TEC] < 128 + ) # and should be more than error passive threshold + # error warning count should stay the same, as we went "down" in severity not up + print( + "no new warning", warning_counters[_IDX_NUM_WARNING] == passive_counters[_IDX_NUM_WARNING] + ) + print( + "no new passive", warning_counters[_IDX_NUM_PASSIVE] == passive_counters[_IDX_NUM_PASSIVE] + ) + + # Tell the receiver to change to the wrong baud rate, which should create both RX and TX errorxs + multitest.broadcast("switch baud") + multitest.wait("switched baud") + + # queue another message. This will keep trying to send until we revert back to ERROR_PASSIVE + idx = can.send(_ID, b"YETMORE") + print("queued yetmore", idx is not None) + while can.state() != CAN.STATE_PASSIVE: + pass + + print(state_name(can.state())) # should be ERROR_PASSIVE again + passive_counters = can.get_counters() + # print(passive_counters) # DEBUG + # we can't say for sure which error counter will hit the ERROR_PASSIVE threshold first + print( + "one over thresh", passive_counters[_IDX_TEC] >= 128 or passive_counters[_IDX_REC] >= 128 + ) + print( + "no new warning", passive_counters[_IDX_NUM_WARNING] == warning_counters[_IDX_NUM_WARNING] + ) + print( + "counted passive", + passive_counters[_IDX_NUM_PASSIVE] == warning_counters[_IDX_NUM_PASSIVE] + 1, + ) + + # Note that we can't get all the way to the most severe BUS_OFF error state + # with this test setup, as Bus Off requires more than just "normal" frame + # transmit errors. + + # restarting the controller may cause it to leave its error state, or not, depending + # on the implementation - but it shouldn't cause any recovery issues. Also cancels all pending TX + # (note: have to do this before 'fix baud' or we create a race condition for pending tx) + can.restart() + + # tell the receiver to go back to a valid baud rate + multitest.broadcast("fix baud") + multitest.wait("fixed baud") + + idx_more = can.send(_ID, b"MOREMORE") + time.sleep_ms(50) # irq_sender should fire during this window + print("queued moremore", idx_more is not None) + + print("done") diff --git a/tests/multi_extmod/machine_can_07_error_states.py.exp b/tests/multi_extmod/machine_can_07_error_states.py.exp new file mode 100644 index 00000000000..158ae63bfbc --- /dev/null +++ b/tests/multi_extmod/machine_can_07_error_states.py.exp @@ -0,0 +1,37 @@ +--- instance0 --- +recv 0x100 5041594c4f4144 +can deinit() +can init() +can switch baud +sending bad msg +sending cancelling +cancelled True +re-init +done +--- instance1 --- +started STATE_ACTIVE +irq sent True +irq state 2 +irq state 3 +STATE_PASSIVE +tec increased True +tec over thresh True +counted warning True +counted passive True +some pending tx True +irq sent True +irq sent True +STATE_WARNING +tec decreased True +tec below thresh True +no new warning True +no new passive True +irq state 3 +queued yetmore True +STATE_PASSIVE +one over thresh True +no new warning True +counted passive True +irq sent True +queued moremore True +done diff --git a/tests/multi_extmod/machine_can_08_init_mode.py b/tests/multi_extmod/machine_can_08_init_mode.py new file mode 100644 index 00000000000..459e6180ab6 --- /dev/null +++ b/tests/multi_extmod/machine_can_08_init_mode.py @@ -0,0 +1,102 @@ +from machine import CAN +from micropython import const +import time + +# instance0 transitions through various modes, instance1 +# listens for various messages (or not) +# +# Note this test assumes no other CAN nodes are connected apart from the test +# instances (or if they are connected they must be in silent mode.) +# +# TODO: This test needs to eventually support the case where modes aren't supported +# on a controller, maybe by printing fake output if the mode switch fails? + +# MODE_NORMAL, MODE_SLEEP, MODE_LOOPBACK, MODE_SILENT, MODE_SILENT_LOOPBACK +can = CAN(1, 500_000, mode=CAN.MODE_NORMAL) + +# While instance0 is in Silent mode, instance1 sends a message with this ID +# that will be retried for 100ms (as instance0 won't ACK). So don't print every one. +_SILENT_RX_ID = const(0x53) +silent_rx_count = 0 + + +def irq_print(can): + global silent_rx_count + while flags := can.irq().flags(): + if flags & can.IRQ_RX: + can_id, data, _flags, errors = can.recv() + if can_id != _SILENT_RX_ID: + print("recv", hex(can_id), bytes(data)) + else: + silent_rx_count += 1 + if flags & can.IRQ_TX: # note: only enabled on instance1 to avoid race conditions + print("send", "failed" if flags & can.IRQ_TX_FAILED else "ok") + + +def reinit_with_mode(mode): + can.deinit() + can.init(bitrate=500_000, mode=mode) + can.irq(irq_print, trigger=can.IRQ_RX, hard=False) + can.set_filters(None) # receive all + + +def instance0(): + multitest.next() + multitest.wait("instance1 ready") + + reinit_with_mode(can.MODE_NORMAL) + print("Normal", "MODE_NORMAL" in str(can)) + can.send(0x50, b"Normal") + time.sleep_ms(100) + + # Skipping MODE_SLEEP as means different things on different hardware + + reinit_with_mode(can.MODE_LOOPBACK) + print("Loopback", "MODE_LOOPBACK" in str(can)) + + # This message should go out to the bus, but will also be received by instance0 itself + can.send(0x51, b"Loopback") + time.sleep_ms(100) + + reinit_with_mode(can.MODE_SILENT) + print("Silent", "MODE_SLIENT" in str(can)) + + # This message shouldn't go out onto the bus + idx = can.send(0x52, b"Silent") + multitest.broadcast("silent") + multitest.wait("silent done") + # we should have received the message from instance1 many times, as instance0 won't have ACKed it + print("silent_rx_count", silent_rx_count > 5) + can.cancel_send(idx) + + reinit_with_mode(can.MODE_SILENT_LOOPBACK) + print("Silent Loopback", "MODE_SILENT_LOOPBACK" in str(can)) + + # This message should be received by instance0 only + idx = can.send(0x54, b"SiLoop") + time.sleep_ms(50) + + reinit_with_mode(can.MODE_NORMAL) + print("Normal again", "MODE_NORMAL" in str(can)) + can.send(0x55, b"Normal2") # should be received by instance1 only, again + multitest.broadcast("normal done") + + +# Receiver +def instance1(): + can.irq(irq_print, trigger=can.IRQ_RX | can.IRQ_TX, hard=False) + can.set_filters(None) # receive all + multitest.next() + + multitest.broadcast("instance1 ready") + + # The IRQ does most of the work on this instance + + multitest.wait("silent") + # Sending this message back, it should fail to send as Silent mode won't ACK it + idx = can.send(0x53, b"Silent2") + time.sleep_ms(20) + can.cancel_send(idx) + multitest.broadcast("silent done") + + multitest.wait("normal done") diff --git a/tests/multi_extmod/machine_can_08_init_mode.py.exp b/tests/multi_extmod/machine_can_08_init_mode.py.exp new file mode 100644 index 00000000000..b9f0f11ae10 --- /dev/null +++ b/tests/multi_extmod/machine_can_08_init_mode.py.exp @@ -0,0 +1,14 @@ +--- instance0 --- +Normal True +Loopback True +recv 0x51 b'Loopback' +Silent False +silent_rx_count True +Silent Loopback True +recv 0x54 b'SiLoop' +Normal again True +--- instance1 --- +recv 0x50 b'Normal' +recv 0x51 b'Loopback' +send failed +recv 0x55 b'Normal2' diff --git a/tests/multi_extmod/machine_i2c_target_irq.py b/tests/multi_extmod/machine_i2c_target_irq.py new file mode 100644 index 00000000000..eafd9dfdca8 --- /dev/null +++ b/tests/multi_extmod/machine_i2c_target_irq.py @@ -0,0 +1,137 @@ +# Test I2CTarget IRQs and clock stretching. +# +# Requires two instances with their SCL and SDA lines connected together. +# Any combination of the below supported boards can be used. +# +# Notes: +# - pull-up resistors may be needed +# - alif use 1.8V signalling + +import sys +import time +from machine import I2C, I2CTarget + +if not hasattr(I2CTarget, "IRQ_ADDR_MATCH_READ"): + print("SKIP") + raise SystemExit + +ADDR = 67 +clock_stretch_us = 200 + +# Configure pins based on the target. +if sys.platform == "alif": + i2c_args = (1,) # pins P3_7/P3_6 + i2c_kwargs = {} +elif sys.platform == "mimxrt": + i2c_args = (0,) # pins 19/18 on Teensy 4.x + i2c_kwargs = {} + clock_stretch_us = 50 # mimxrt cannot delay too long in the IRQ handler +elif sys.platform == "rp2": + i2c_args = (0,) + i2c_kwargs = {"scl": 9, "sda": 8} +elif sys.platform == "pyboard": + i2c_args = ("Y",) + i2c_kwargs = {} +elif sys.platform == "samd": + i2c_args = () # pins SCL/SDA + i2c_kwargs = {} +elif "zephyr-rpi_pico" in sys.implementation._machine: + i2c_args = ("i2c1",) # on gpio7/gpio6 + i2c_kwargs = {} +else: + print("Please add support for this test on this platform.") + raise SystemExit + + +def simple_irq(i2c_target): + flags = i2c_target.irq().flags() + if flags & I2CTarget.IRQ_ADDR_MATCH_READ: + print("IRQ_ADDR_MATCH_READ") + if flags & I2CTarget.IRQ_ADDR_MATCH_WRITE: + print("IRQ_ADDR_MATCH_WRITE") + + # Force clock stretching. + time.sleep_us(clock_stretch_us) + + +class I2CTargetMemory: + def __init__(self, i2c_target, mem): + self.buf1 = bytearray(1) + self.mem = mem + self.memaddr = 0 + self.state = 0 + i2c_target.irq( + self.irq, + I2CTarget.IRQ_ADDR_MATCH_WRITE | I2CTarget.IRQ_READ_REQ | I2CTarget.IRQ_WRITE_REQ, + hard=True, + ) + + def irq(self, i2c_target): + # Force clock stretching. + time.sleep_us(clock_stretch_us) + + flags = i2c_target.irq().flags() + if flags & I2CTarget.IRQ_ADDR_MATCH_WRITE: + self.state = 0 + if flags & I2CTarget.IRQ_READ_REQ: + self.buf1[0] = self.mem[self.memaddr] + self.memaddr += 1 + i2c_target.write(self.buf1) + if flags & I2CTarget.IRQ_WRITE_REQ: + i2c_target.readinto(self.buf1) + if self.state == 0: + self.state = 1 + self.memaddr = self.buf1[0] + else: + self.mem[self.memaddr] = self.buf1[0] + self.memaddr += 1 + self.memaddr %= len(self.mem) + + # Force clock stretching. + time.sleep_us(clock_stretch_us) + + +# I2C controller +def instance0(): + i2c = I2C(*i2c_args, **i2c_kwargs) + multitest.next() + for iteration in range(2): + print("controller iteration", iteration) + multitest.wait("target stage 1") + i2c.writeto_mem(ADDR, 2, "0123") + multitest.broadcast("controller stage 2") + multitest.wait("target stage 3") + print(i2c.readfrom_mem(ADDR, 2, 4)) + multitest.broadcast("controller stage 4") + print("done") + + +# I2C target +def instance1(): + multitest.next() + + for iteration in range(2): + print("target iteration", iteration) + buf = bytearray(b"--------") + if iteration == 0: + # Use built-in memory capability of I2CTarget. + i2c_target = I2CTarget(*i2c_args, **i2c_kwargs, addr=ADDR, mem=buf) + i2c_target.irq( + simple_irq, + I2CTarget.IRQ_ADDR_MATCH_READ | I2CTarget.IRQ_ADDR_MATCH_WRITE, + hard=True, + ) + else: + # Implement a memory device by hand. + i2c_target = I2CTarget(*i2c_args, **i2c_kwargs, addr=ADDR) + I2CTargetMemory(i2c_target, buf) + + multitest.broadcast("target stage 1") + multitest.wait("controller stage 2") + print(buf) + multitest.broadcast("target stage 3") + multitest.wait("controller stage 4") + + i2c_target.deinit() + + print("done") diff --git a/tests/multi_extmod/machine_i2c_target_irq.py.exp b/tests/multi_extmod/machine_i2c_target_irq.py.exp new file mode 100644 index 00000000000..a17c8f43858 --- /dev/null +++ b/tests/multi_extmod/machine_i2c_target_irq.py.exp @@ -0,0 +1,15 @@ +--- instance0 --- +controller iteration 0 +b'0123' +controller iteration 1 +b'0123' +done +--- instance1 --- +target iteration 0 +IRQ_ADDR_MATCH_WRITE +bytearray(b'--0123--') +IRQ_ADDR_MATCH_WRITE +IRQ_ADDR_MATCH_READ +target iteration 1 +bytearray(b'--0123--') +done diff --git a/tests/multi_extmod/machine_i2c_target_memory.py b/tests/multi_extmod/machine_i2c_target_memory.py new file mode 100644 index 00000000000..6b3f0d03eb7 --- /dev/null +++ b/tests/multi_extmod/machine_i2c_target_memory.py @@ -0,0 +1,79 @@ +# Test basic use of I2CTarget and a memory buffer. +# +# Requires two instances with their SCL and SDA lines connected together. +# Any combination of the below supported boards can be used. +# +# Notes: +# - pull-up resistors may be needed +# - alif use 1.8V signalling + +import sys +from machine import I2C, I2CTarget + +ADDR = 67 + +# Configure pins based on the target. +if sys.platform == "alif": + i2c_args = (1,) # pins P3_7/P3_6 + i2c_kwargs = {} +elif sys.platform == "esp32": + i2c_args = (1,) # on pins 9/8 + i2c_kwargs = {} +elif sys.platform == "mimxrt": + i2c_args = (0,) # pins 19/18 on Teensy 4.x + i2c_kwargs = {} +elif sys.platform == "rp2": + i2c_args = (0,) + i2c_kwargs = {"scl": 9, "sda": 8} +elif sys.platform == "pyboard": + i2c_args = ("Y",) + i2c_kwargs = {} +elif sys.platform == "samd": + i2c_args = () # pins SCL/SDA + i2c_kwargs = {} +elif "zephyr-rpi_pico" in sys.implementation._machine: + i2c_args = ("i2c1",) # on gpio7/gpio6 + i2c_kwargs = {} +else: + print("Please add support for this test on this platform.") + raise SystemExit + + +def simple_irq(i2c_target): + flags = i2c_target.irq().flags() + if flags & I2CTarget.IRQ_END_READ: + print("IRQ_END_READ", i2c_target.memaddr) + if flags & I2CTarget.IRQ_END_WRITE: + print("IRQ_END_WRITE", i2c_target.memaddr) + + +# I2C controller +def instance0(): + i2c = I2C(*i2c_args, **i2c_kwargs) + multitest.next() + for iteration in range(2): + print("controller iteration", iteration) + multitest.wait("target stage 1") + i2c.writeto_mem(ADDR, 2 + iteration, "0123") + multitest.broadcast("controller stage 2") + multitest.wait("target stage 3") + print(i2c.readfrom_mem(ADDR, 2 + iteration, 4)) + multitest.broadcast("controller stage 4") + print("done") + + +# I2C target +def instance1(): + buf = bytearray(b"--------") + i2c_target = I2CTarget(*i2c_args, **i2c_kwargs, addr=ADDR, mem=buf) + i2c_target.irq(simple_irq) + multitest.next() + for iteration in range(2): + print("target iteration", iteration) + multitest.broadcast("target stage 1") + multitest.wait("controller stage 2") + print(buf) + multitest.broadcast("target stage 3") + multitest.wait("controller stage 4") + i2c_target.deinit() + print("done") diff --git a/tests/multi_extmod/machine_i2c_target_memory.py.exp b/tests/multi_extmod/machine_i2c_target_memory.py.exp new file mode 100644 index 00000000000..71386cfe769 --- /dev/null +++ b/tests/multi_extmod/machine_i2c_target_memory.py.exp @@ -0,0 +1,16 @@ +--- instance0 --- +controller iteration 0 +b'0123' +controller iteration 1 +b'0123' +done +--- instance1 --- +target iteration 0 +IRQ_END_WRITE 2 +bytearray(b'--0123--') +IRQ_END_READ 2 +target iteration 1 +IRQ_END_WRITE 3 +bytearray(b'--00123-') +IRQ_END_READ 3 +done diff --git a/tests/multi_net/asyncio_tcp_readinto.py b/tests/multi_net/asyncio_tcp_readinto.py index 563b640e13b..972195f96dc 100644 --- a/tests/multi_net/asyncio_tcp_readinto.py +++ b/tests/multi_net/asyncio_tcp_readinto.py @@ -1,13 +1,8 @@ # Test asyncio stream readinto() method using TCP server/client -try: - import asyncio -except ImportError: - print("SKIP") - raise SystemExit - try: import array + import asyncio except ImportError: print("SKIP") raise SystemExit diff --git a/tests/multi_net/asyncio_tls_server_client.py b/tests/multi_net/asyncio_tls_server_client.py index 98f15c6625f..959dcfe5994 100644 --- a/tests/multi_net/asyncio_tls_server_client.py +++ b/tests/multi_net/asyncio_tls_server_client.py @@ -8,19 +8,16 @@ print("SKIP") raise SystemExit +if not hasattr(ssl, "CERT_REQUIRED"): + print("SKIP") + raise SystemExit + PORT = 8000 # These are test certificates. See tests/README.md for details. cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - async def handle_connection(reader, writer): data = await reader.read(100) diff --git a/tests/multi_net/asyncio_tls_server_client_cert_required_error.py b/tests/multi_net/asyncio_tls_server_client_cert_required_error.py index 178ad392740..5d66d9c1bd9 100644 --- a/tests/multi_net/asyncio_tls_server_client_cert_required_error.py +++ b/tests/multi_net/asyncio_tls_server_client_cert_required_error.py @@ -8,19 +8,16 @@ print("SKIP") raise SystemExit +if not hasattr(ssl, "CERT_REQUIRED"): + print("SKIP") + raise SystemExit + PORT = 8000 # These are test certificates. See tests/README.md for details. cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - async def handle_connection(reader, writer): print("handle connection") diff --git a/tests/multi_net/asyncio_tls_server_client_readline.py b/tests/multi_net/asyncio_tls_server_client_readline.py index da5f1afee2a..539eae23d5c 100644 --- a/tests/multi_net/asyncio_tls_server_client_readline.py +++ b/tests/multi_net/asyncio_tls_server_client_readline.py @@ -8,19 +8,16 @@ print("SKIP") raise SystemExit +if not hasattr(ssl, "CERT_REQUIRED"): + print("SKIP") + raise SystemExit + PORT = 8000 # These are test certificates. See tests/README.md for details. cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - async def handle_connection(reader, writer): data = await reader.readline() diff --git a/tests/multi_net/asyncio_tls_server_client_verify_error.py b/tests/multi_net/asyncio_tls_server_client_verify_error.py index 362f0fc8ecf..50169dd679a 100644 --- a/tests/multi_net/asyncio_tls_server_client_verify_error.py +++ b/tests/multi_net/asyncio_tls_server_client_verify_error.py @@ -8,19 +8,16 @@ print("SKIP") raise SystemExit +if not hasattr(ssl, "CERT_REQUIRED"): + print("SKIP") + raise SystemExit + PORT = 8000 # These are test certificates. See tests/README.md for details. cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - async def handle_connection(reader, writer): print("handle connection") diff --git a/tests/multi_net/ec_cert.der b/tests/multi_net/ec_cert.der index a503a39dfe4..a13f1c984a6 100644 Binary files a/tests/multi_net/ec_cert.der and b/tests/multi_net/ec_cert.der differ diff --git a/tests/multi_net/ec_key.der b/tests/multi_net/ec_key.der index 7d50fe3bcda..f136665755c 100644 Binary files a/tests/multi_net/ec_key.der and b/tests/multi_net/ec_key.der differ diff --git a/tests/multi_net/rsa_cert.der b/tests/multi_net/rsa_cert.der index d0ea34bf4d6..fc15ef9dae9 100644 Binary files a/tests/multi_net/rsa_cert.der and b/tests/multi_net/rsa_cert.der differ diff --git a/tests/multi_net/rsa_key.der b/tests/multi_net/rsa_key.der index c9c535ae639..658af9bd899 100644 Binary files a/tests/multi_net/rsa_key.der and b/tests/multi_net/rsa_key.der differ diff --git a/tests/multi_net/ssl_cert_ec.py b/tests/multi_net/ssl_cert_ec.py index 2c5734e0526..cb4e36f78cc 100644 --- a/tests/multi_net/ssl_cert_ec.py +++ b/tests/multi_net/ssl_cert_ec.py @@ -7,19 +7,16 @@ print("SKIP") raise SystemExit +if not hasattr(ssl, "CERT_REQUIRED"): + print("SKIP") + raise SystemExit + PORT = 8000 # These are test certificates. See tests/README.md for details. certfile = "ec_cert.der" keyfile = "ec_key.der" -try: - os.stat(certfile) - os.stat(keyfile) -except OSError: - print("SKIP") - raise SystemExit - with open(certfile, "rb") as cf: cert = cadata = cf.read() diff --git a/tests/multi_net/ssl_cert_rsa.py b/tests/multi_net/ssl_cert_rsa.py index d148c8ebcfe..473a5b16f8d 100644 --- a/tests/multi_net/ssl_cert_rsa.py +++ b/tests/multi_net/ssl_cert_rsa.py @@ -7,19 +7,16 @@ print("SKIP") raise SystemExit +if not hasattr(ssl, "CERT_REQUIRED"): + print("SKIP") + raise SystemExit + PORT = 8000 # These are test certificates. See tests/README.md for details. certfile = "rsa_cert.der" keyfile = "rsa_key.der" -try: - os.stat(certfile) - os.stat(keyfile) -except OSError: - print("SKIP") - raise SystemExit - with open(certfile, "rb") as cf: cert = cadata = cf.read() diff --git a/tests/multi_net/sslcontext_check_hostname_error.py b/tests/multi_net/sslcontext_check_hostname_error.py index d85363f00bd..65dff998e1f 100644 --- a/tests/multi_net/sslcontext_check_hostname_error.py +++ b/tests/multi_net/sslcontext_check_hostname_error.py @@ -8,19 +8,16 @@ print("SKIP") raise SystemExit +if not hasattr(ssl, "CERT_REQUIRED"): + print("SKIP") + raise SystemExit + PORT = 8000 # These are test certificates. See tests/README.md for details. cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - # Server def instance0(): diff --git a/tests/multi_net/sslcontext_getpeercert.py b/tests/multi_net/sslcontext_getpeercert.py index e9d96be248c..89a12753bb4 100644 --- a/tests/multi_net/sslcontext_getpeercert.py +++ b/tests/multi_net/sslcontext_getpeercert.py @@ -9,19 +9,16 @@ print("SKIP") raise SystemExit +if not hasattr(ssl, "CERT_REQUIRED"): + print("SKIP") + raise SystemExit + PORT = 8000 # These are test certificates. See tests/README.md for details. cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - # Server def instance0(): diff --git a/tests/multi_net/sslcontext_getpeercert.py.exp b/tests/multi_net/sslcontext_getpeercert.py.exp index e7a0ab0b462..a0dfaa4f567 100644 --- a/tests/multi_net/sslcontext_getpeercert.py.exp +++ b/tests/multi_net/sslcontext_getpeercert.py.exp @@ -1,5 +1,5 @@ --- instance0 --- b'client to server' --- instance1 --- -308201d330820179a00302010202144315a7cd8f69febe2640314e7c97d60a2523ad15300a06082a8648ce3d040302303f311a301806035504030c116d6963726f707974686f6e2e6c6f63616c31143012060355040a0c0b4d6963726f507974686f6e310b3009060355040613024155301e170d3234303131343034353335335a170d3235303131333034353335335a303f311a301806035504030c116d6963726f707974686f6e2e6c6f63616c31143012060355040a0c0b4d6963726f507974686f6e310b30090603550406130241553059301306072a8648ce3d020106082a8648ce3d0301070342000449b7f5fa687cb25a9464c397508149992f445c860bcf7002958eb4337636c6af840cd4c8cf3b96f2384860d8ae3ee3fa135dba051e8605e62bd871689c6af43ca3533051301d0603551d0e0416041441b3ae171d91e330411d8543ba45e0f2d5b2951b301f0603551d2304183016801441b3ae171d91e330411d8543ba45e0f2d5b2951b300f0603551d130101ff040530030101ff300a06082a8648ce3d04030203480030450220587f61c34739d6fab5802a674dcc54443ae9c87da374078c4ee1cd83f4ad1694022100cfc45dcf264888c6ba2c36e78bd27bb67856d7879a052dd7aa7ecf7215f7b992 +308201d230820179a003020102021403abf0266b125f8ec8dab57c34d655e94ae4d9bb300a06082a8648ce3d040302303f311a301806035504030c116d6963726f707974686f6e2e6c6f63616c31143012060355040a0c0b4d6963726f507974686f6e310b3009060355040613024155301e170d3235303131343033333531375a170d3335303131323033333531375a303f311a301806035504030c116d6963726f707974686f6e2e6c6f63616c31143012060355040a0c0b4d6963726f507974686f6e310b30090603550406130241553059301306072a8648ce3d020106082a8648ce3d030107034200042882eb6055f540ddef989ec7ead84281461436745d1b9174a2730f2bc74cf91e1ce62d4ada67129e891ab70e73a34b644b000d8d1e4c43189a3390210f447169a3533051301d0603551d0e0416041410ced497be3800e7b19008504efd0adc39bfe02a301f0603551d2304183016801410ced497be3800e7b19008504efd0adc39bfe02a300f0603551d130101ff040530030101ff300a06082a8648ce3d040302034700304402204b153d2851e7846351383beac1d5b39791810156b0f9672040d3499e14041969022013e32fe207d3bccb7f9859b3140e73074f656c034679135e517b1e9cb05ae5b2 b'server to client' diff --git a/tests/multi_net/sslcontext_server_client.py b/tests/multi_net/sslcontext_server_client.py index 6516de53f7d..1b6a0996688 100644 --- a/tests/multi_net/sslcontext_server_client.py +++ b/tests/multi_net/sslcontext_server_client.py @@ -8,20 +8,20 @@ print("SKIP") raise SystemExit +if not hasattr(ssl, "CERT_REQUIRED"): + print("SKIP") + raise SystemExit + PORT = 8000 # These are test certificates. See tests/README.md for details. certfile = "ec_cert.der" keyfile = "ec_key.der" -try: - with open(certfile, "rb") as cf: - cert = cadata = cf.read() - with open(keyfile, "rb") as kf: - key = kf.read() -except OSError: - print("SKIP") - raise SystemExit +with open(certfile, "rb") as cf: + cert = cadata = cf.read() +with open(keyfile, "rb") as kf: + key = kf.read() # Server diff --git a/tests/multi_net/sslcontext_server_client_ciphers.py b/tests/multi_net/sslcontext_server_client_ciphers.py index 3334d9d9e4c..cd3fe919a34 100644 --- a/tests/multi_net/sslcontext_server_client_ciphers.py +++ b/tests/multi_net/sslcontext_server_client_ciphers.py @@ -8,20 +8,20 @@ print("SKIP") raise SystemExit +if not hasattr(tls, "CERT_REQUIRED"): + print("SKIP") + raise SystemExit + PORT = 8000 # These are test certificates. See tests/README.md for details. cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - with open(cafile, "rb") as f: - cadata = f.read() - with open(key, "rb") as f: - keydata = f.read() -except OSError: - print("SKIP") - raise SystemExit +with open(cafile, "rb") as f: + cadata = f.read() +with open(key, "rb") as f: + keydata = f.read() # Server diff --git a/tests/multi_net/sslcontext_server_client_files.py b/tests/multi_net/sslcontext_server_client_files.py index 64a4215c75b..e42b9ea033a 100644 --- a/tests/multi_net/sslcontext_server_client_files.py +++ b/tests/multi_net/sslcontext_server_client_files.py @@ -8,19 +8,16 @@ print("SKIP") raise SystemExit +if not hasattr(ssl, "CERT_REQUIRED"): + print("SKIP") + raise SystemExit + PORT = 8000 # These are test certificates. See tests/README.md for details. cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - # Server def instance0(): diff --git a/tests/multi_net/sslcontext_verify_callback.py b/tests/multi_net/sslcontext_verify_callback.py index 74b97382453..b762b95e6f0 100644 --- a/tests/multi_net/sslcontext_verify_callback.py +++ b/tests/multi_net/sslcontext_verify_callback.py @@ -9,20 +9,20 @@ print("SKIP") raise SystemExit +if not hasattr(tls, "CERT_REQUIRED"): + print("SKIP") + raise SystemExit + PORT = 8000 # These are test certificates. See tests/README.md for details. cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - with open(cafile, "rb") as f: - cadata = f.read() - with open(key, "rb") as f: - key = f.read() -except OSError: - print("SKIP") - raise SystemExit +with open(cafile, "rb") as f: + cadata = f.read() +with open(key, "rb") as f: + key = f.read() def verify_callback(cert, depth): diff --git a/tests/multi_net/sslcontext_verify_callback.py.exp b/tests/multi_net/sslcontext_verify_callback.py.exp index e7a0ab0b462..a0dfaa4f567 100644 --- a/tests/multi_net/sslcontext_verify_callback.py.exp +++ b/tests/multi_net/sslcontext_verify_callback.py.exp @@ -1,5 +1,5 @@ --- instance0 --- b'client to server' --- instance1 --- -308201d330820179a00302010202144315a7cd8f69febe2640314e7c97d60a2523ad15300a06082a8648ce3d040302303f311a301806035504030c116d6963726f707974686f6e2e6c6f63616c31143012060355040a0c0b4d6963726f507974686f6e310b3009060355040613024155301e170d3234303131343034353335335a170d3235303131333034353335335a303f311a301806035504030c116d6963726f707974686f6e2e6c6f63616c31143012060355040a0c0b4d6963726f507974686f6e310b30090603550406130241553059301306072a8648ce3d020106082a8648ce3d0301070342000449b7f5fa687cb25a9464c397508149992f445c860bcf7002958eb4337636c6af840cd4c8cf3b96f2384860d8ae3ee3fa135dba051e8605e62bd871689c6af43ca3533051301d0603551d0e0416041441b3ae171d91e330411d8543ba45e0f2d5b2951b301f0603551d2304183016801441b3ae171d91e330411d8543ba45e0f2d5b2951b300f0603551d130101ff040530030101ff300a06082a8648ce3d04030203480030450220587f61c34739d6fab5802a674dcc54443ae9c87da374078c4ee1cd83f4ad1694022100cfc45dcf264888c6ba2c36e78bd27bb67856d7879a052dd7aa7ecf7215f7b992 +308201d230820179a003020102021403abf0266b125f8ec8dab57c34d655e94ae4d9bb300a06082a8648ce3d040302303f311a301806035504030c116d6963726f707974686f6e2e6c6f63616c31143012060355040a0c0b4d6963726f507974686f6e310b3009060355040613024155301e170d3235303131343033333531375a170d3335303131323033333531375a303f311a301806035504030c116d6963726f707974686f6e2e6c6f63616c31143012060355040a0c0b4d6963726f507974686f6e310b30090603550406130241553059301306072a8648ce3d020106082a8648ce3d030107034200042882eb6055f540ddef989ec7ead84281461436745d1b9174a2730f2bc74cf91e1ce62d4ada67129e891ab70e73a34b644b000d8d1e4c43189a3390210f447169a3533051301d0603551d0e0416041410ced497be3800e7b19008504efd0adc39bfe02a301f0603551d2304183016801410ced497be3800e7b19008504efd0adc39bfe02a300f0603551d130101ff040530030101ff300a06082a8648ce3d040302034700304402204b153d2851e7846351383beac1d5b39791810156b0f9672040d3499e14041969022013e32fe207d3bccb7f9859b3140e73074f656c034679135e517b1e9cb05ae5b2 b'server to client' diff --git a/tests/multi_net/sslcontext_verify_error.py b/tests/multi_net/sslcontext_verify_error.py index 5dc461e7708..f6e1477bfed 100644 --- a/tests/multi_net/sslcontext_verify_error.py +++ b/tests/multi_net/sslcontext_verify_error.py @@ -8,19 +8,16 @@ print("SKIP") raise SystemExit +if not hasattr(ssl, "CERT_REQUIRED"): + print("SKIP") + raise SystemExit + PORT = 8000 # These are test certificates. See tests/README.md for details. cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - # Server def instance0(): diff --git a/tests/multi_net/sslcontext_verify_time_error.py b/tests/multi_net/sslcontext_verify_time_error.py index fbefdecf9f4..274d99d4bf6 100644 --- a/tests/multi_net/sslcontext_verify_time_error.py +++ b/tests/multi_net/sslcontext_verify_time_error.py @@ -8,19 +8,16 @@ print("SKIP") raise SystemExit +if not hasattr(ssl, "CERT_REQUIRED"): + print("SKIP") + raise SystemExit + PORT = 8000 # These are test certificates. See tests/README.md for details. cert = cafile = "expired_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - # Server def instance0(): diff --git a/tests/multi_net/tcp_accept_recv.py b/tests/multi_net/tcp_accept_recv.py index dee14e3b977..4108a6f8a3e 100644 --- a/tests/multi_net/tcp_accept_recv.py +++ b/tests/multi_net/tcp_accept_recv.py @@ -1,30 +1,73 @@ -# Test recv on socket that just accepted a connection +# Test recv on listening socket after accept(), with various listen() arguments import socket PORT = 8000 +# Test cases for listen() function +LISTEN_ARGS = [None, 0, 1, 2] # None means no argument + # Server def instance0(): multitest.globals(IP=multitest.get_network_ip()) - s = socket.socket() - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1]) - s.listen(1) multitest.next() - s.accept() - try: - print("recv", s.recv(10)) # should raise Errno 107 ENOTCONN - except OSError as er: - print(er.errno in (107, 128)) - s.close() + + test_num = 0 + for blocking_mode in [True, False]: + for listen_arg in LISTEN_ARGS: + test_num += 1 + s = socket.socket() + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1]) + + # Call listen with or without argument based on test case + if listen_arg is None: + print(f"Test case {test_num}/8: listen() blocking={blocking_mode}") + s.listen() + else: + print(f"Test case {test_num}/8: listen({listen_arg}) blocking={blocking_mode}") + s.listen(listen_arg) + + # Signal client that server is ready + multitest.broadcast(f"server_ready_{test_num}") + + # Wait for client connection + c, _ = s.accept() + + # Set blocking mode after accept + s.setblocking(blocking_mode) + + try: + print("recv", s.recv(10)) # should raise Errno 107 ENOTCONN + except OSError as er: + # Verify the error code is either 107 (ENOTCONN) or 128 (ENOTCONN on Windows) + print(er.errno in (107, 128)) + + # Cleanup + c.close() + s.close() + + # Signal client we're done with this test case + multitest.broadcast(f"server_done_{test_num}") # Client def instance1(): multitest.next() - s = socket.socket() - s.connect(socket.getaddrinfo(IP, PORT)[0][-1]) - s.send(b"GET / HTTP/1.0\r\n\r\n") - s.close() + + test_num = 0 + for blocking_mode in [True, False]: + for _ in LISTEN_ARGS: + test_num += 1 + # Wait for server to be ready + multitest.wait(f"server_ready_{test_num}") + + # Connect to server + s = socket.socket() + s.connect(socket.getaddrinfo(IP, PORT)[0][-1]) + s.send(b"GET / HTTP/1.0\r\n\r\n") + s.close() + + # Wait for server to finish this test case + multitest.wait(f"server_done_{test_num}") diff --git a/tests/multi_net/tcp_client_rst.py b/tests/multi_net/tcp_client_rst.py index 1fe994f36ce..6e74edc194d 100644 --- a/tests/multi_net/tcp_client_rst.py +++ b/tests/multi_net/tcp_client_rst.py @@ -2,6 +2,12 @@ import struct, time, socket, select +try: + import errno +except ImportError: + print("SKIP") + raise SystemExit + PORT = 8000 @@ -18,27 +24,49 @@ def instance0(): s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1]) s.listen(1) multitest.next() - s2, _ = s.accept() - s2.setblocking(False) - poll = select.poll() - poll.register(s2, select.POLLIN) - time.sleep(0.4) - print(convert_poll_list(poll.poll(1000))) - # TODO: the following recv don't work with lwip, it abandons data upon TCP RST - try: - print(s2.recv(10)) + + for iteration in range(2): + print("server iteration", iteration) + + # Accept a connection from the client. + s2, _ = s.accept() + s2.setblocking(False) + + # Create a poller for the connected socket. + poll = select.poll() + poll.register(s2, select.POLLIN) + + # Wait for the client to send data and issue a TCP RST. + for _ in range(10): + if select.POLLIN | select.POLLERR | select.POLLHUP in convert_poll_list( + poll.poll(1000) + ): + break + time.sleep(0.1) print(convert_poll_list(poll.poll(1000))) - print(s2.recv(10)) + + # On the second test, drain the incoming data. + if iteration == 1: + for _ in range(5): + try: + print(s2.recv(10)) + except OSError as er: + # This error should only happen on the 3rd recv. + print("ECONNRESET:", er.errno == errno.ECONNRESET) + print(convert_poll_list(poll.poll(1000))) + + # Close the connection to the client. + s2.close() print(convert_poll_list(poll.poll(1000))) - print(s2.recv(10)) + + # Try to read more data from the closed socket, to make sure the error code is correct. + try: + print(s2.recv(10)) + except OSError as er: + print("EBADF:", er.errno == errno.EBADF) print(convert_poll_list(poll.poll(1000))) - except OSError as er: - print(er.errno) - print(convert_poll_list(poll.poll(1000))) - # TODO lwip raises here but apparently it shouldn't - print(s2.recv(10)) - print(convert_poll_list(poll.poll(1000))) - s2.close() + + # Close the listening socket. s.close() @@ -47,11 +75,13 @@ def instance1(): if not hasattr(socket, "SO_LINGER"): multitest.skip() multitest.next() - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) - s.connect(socket.getaddrinfo(IP, PORT)[0][-1]) - lgr_onoff = 1 - lgr_linger = 0 - s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", lgr_onoff, lgr_linger)) - s.send(b"GET / HTTP/1.0\r\n\r\n") - time.sleep(0.2) - s.close() # This issues a TCP RST since we've set the linger option + for iteration in range(2): + print("client iteration", iteration) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + s.connect(socket.getaddrinfo(IP, PORT)[0][-1]) + lgr_onoff = 1 + lgr_linger = 0 + s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", lgr_onoff, lgr_linger)) + s.send(b"GET / HTTP/1.0\r\n\r\n") + time.sleep(0.2) + s.close() # This issues a TCP RST since we've set the linger option diff --git a/tests/multi_net/tcp_recv_peek.py b/tests/multi_net/tcp_recv_peek.py new file mode 100644 index 00000000000..4b8dd6768e4 --- /dev/null +++ b/tests/multi_net/tcp_recv_peek.py @@ -0,0 +1,46 @@ +# Test TCP recv with MSG_PEEK +# +# Note that bare metal LWIP only returns at most one packet's worth of TCP data +# in any recv() call - including when peeking - so can't be too clever with +# different recv() combinations +import socket +import random + + +# Server +def instance0(): + PORT = 10000 + random.getrandbits(15) + multitest.globals(IP=multitest.get_network_ip(), PORT=PORT) + s = socket.socket() + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1]) + s.listen() + multitest.next() + s2, _ = s.accept() + print(s2.recv(8, socket.MSG_PEEK)) + print(s2.recv(8)) + s2.send(b"1234567890") + multitest.broadcast("0-sent") + multitest.wait("1-sent") + print(s2.recv(5, socket.MSG_PEEK)) + print(s2.recv(5, socket.MSG_PEEK)) + multitest.broadcast("0-recved") + multitest.wait("1-recved") # sync here necessary as MP sends RST if closing TCP early + s2.close() + s.close() + + +# Client +def instance1(): + multitest.next() + s = socket.socket() + s.connect(socket.getaddrinfo(IP, PORT)[0][-1]) + s.send(b"abcdefgh") + multitest.broadcast("1-sent") + multitest.wait("0-sent") + s.send(b"klmnopqr") + print(s.recv(5, socket.MSG_PEEK)) + print(s.recv(10)) + multitest.broadcast("1-recved") + multitest.wait("0-recved") + s.close() diff --git a/tests/multi_net/tcp_sendall.py b/tests/multi_net/tcp_sendall.py new file mode 100644 index 00000000000..ee9b780edff --- /dev/null +++ b/tests/multi_net/tcp_sendall.py @@ -0,0 +1,71 @@ +# Simple test of a TCP server and client transferring data using sendall. + +import socket + +if not hasattr(socket.socket, "sendall"): + print("SKIP") + raise SystemExit + +PORT = 8000 + +# This is the smallest send buffer size supported by Linux, see socket(7). +SEND_SIZE = 1024 +BUFFERS = 8 + + +# G. D. Nguyen, "Fast CRCs", vol. 18, no. 10, pp. 1321-1331, Oct. 2009 +# https://dl.acm.org/doi/abs/10.1109/TC.2009.83 +def calculate_checksum(buffer): + checksum = 0 + for byte in buffer: + checksum = (checksum ^ byte) & 0xFFFF + for step in range(16): + if (checksum & 0x8000) != 0: + checksum = ((checksum << 1) ^ 0x8005) & 0xFFFF + else: + checksum = (checksum << 1) & 0xFFFF + return checksum + + +# Server +def instance0(): + multitest.globals(IP=multitest.get_network_ip()) + s = socket.socket() + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1]) + s.listen() + multitest.next() + s2, _ = s.accept() + buffer = b"" + while len(buffer) < SEND_SIZE * BUFFERS: + buffer += s2.recv(SEND_SIZE * BUFFERS) + s2.send(b"%08x" % len(buffer)) + s2.send(b"%04x" % calculate_checksum(buffer)) + s2.close() + s.close() + print("received {} bytes".format(len(buffer))) + + +# Client +def instance1(): + multitest.next() + s = socket.socket() + s.connect(socket.getaddrinfo(IP, PORT)[0][-1]) + # Try our best to trigger a partial send in socket.sendall + if hasattr(socket, "SO_SNDBUF"): + s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, SEND_SIZE) + buffer = bytearray(SEND_SIZE * BUFFERS) + for index in range(len(buffer)): + buffer[index] = index & 0xFF + expected_checksum = calculate_checksum(buffer) + s.sendall(buffer) + print("sent {} bytes".format(len(buffer))) + length = int(s.recv(8), 16) + if length != len(buffer): + raise Exception("sendall len fail (expected %d, got %d)" % (len(buffer), length)) + checksum = int(s.recv(4), 16) + if checksum != expected_checksum: + raise Exception( + "sendall checksum fail (expected %04x, got %04x)" % (expected_checksum, checksum) + ) + s.close() diff --git a/tests/multi_net/tls_dtls_server_client.py b/tests/multi_net/tls_dtls_server_client.py new file mode 100644 index 00000000000..10fac3c6f42 --- /dev/null +++ b/tests/multi_net/tls_dtls_server_client.py @@ -0,0 +1,94 @@ +# Test DTLS server and client, sending a small amount of data between them. + +try: + import socket + import tls +except ImportError: + print("SKIP") + raise SystemExit + +if not hasattr(tls, "PROTOCOL_DTLS_SERVER"): + print("SKIP") + raise SystemExit + +PORT = 8000 + +# These are test certificates. See tests/README.md for details. +certfile = "ec_cert.der" +keyfile = "ec_key.der" + +with open(certfile, "rb") as cf: + cert = cadata = cf.read() +with open(keyfile, "rb") as kf: + key = kf.read() + + +# DTLS server. +def instance0(): + multitest.globals(IP=multitest.get_network_ip()) + + # Create a UDP socket and bind it to accept incoming connections. + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1]) + + multitest.next() + + ctx = tls.SSLContext(tls.PROTOCOL_DTLS_SERVER) + ctx.load_cert_chain(cert, key) + + # Because of "hello verify required", we expect the peer + # to connect twice: once to set the cookie, then second time + # successfully. + # + # As this isn't a real server, we hard-code two connection attempts + for _ in range(2): + print("waiting") + # Wait for the client to connect so we know their address + _, client_addr = s.recvfrom(1, socket.MSG_PEEK) + print("incoming connection") + s.connect(client_addr) # Connect back to the client + + # Wrap the UDP socket in server mode. + try: + s = ctx.wrap_socket(s, server_side=1, client_id=repr(client_addr).encode()) + except OSError as e: + print(e) + continue # wait for second connection + + # Transfer some data. + for i in range(4): + print(s.recv(32)) + s.send(b"server to client " + str(i).encode()) + + # Close the DTLS and UDP connection. + s.close() + break + + +# DTLS client. +def instance1(): + multitest.next() + + # Create a UDP socket and connect to the server. + addr = socket.getaddrinfo(IP, PORT)[0][-1] + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + print("connect") + s.connect(addr) + + # Create a DTLS context and load the certificate. + ctx = tls.SSLContext(tls.PROTOCOL_DTLS_CLIENT) + ctx.verify_mode = tls.CERT_REQUIRED + ctx.load_verify_locations(cadata) + + # Wrap the UDP socket. + print("wrap socket") + s = ctx.wrap_socket(s, server_hostname="micropython.local") + + # Transfer some data. + for i in range(4): + s.send(b"client to server " + str(i).encode()) + print(s.recv(32)) + + # Close the DTLS and UDP connection. + s.close() diff --git a/tests/multi_net/tls_dtls_server_client.py.exp b/tests/multi_net/tls_dtls_server_client.py.exp new file mode 100644 index 00000000000..3de03056740 --- /dev/null +++ b/tests/multi_net/tls_dtls_server_client.py.exp @@ -0,0 +1,17 @@ +--- instance0 --- +waiting +incoming connection +(-27264, 'MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED') +waiting +incoming connection +b'client to server 0' +b'client to server 1' +b'client to server 2' +b'client to server 3' +--- instance1 --- +connect +wrap socket +b'server to client 0' +b'server to client 1' +b'server to client 2' +b'server to client 3' diff --git a/tests/multi_net/udp_data_multi.py b/tests/multi_net/udp_data_multi.py new file mode 100644 index 00000000000..a2ed33d292d --- /dev/null +++ b/tests/multi_net/udp_data_multi.py @@ -0,0 +1,69 @@ +# Test UDP reception when there are multiple incoming UDP packets that need to be +# queued internally in the TCP/IP stack. + +import socket + +NUM_NEW_SOCKETS = 4 +NUM_PACKET_BURSTS = 6 +NUM_PACKET_GROUPS = 4 +TOTAL_PACKET_BURSTS = NUM_NEW_SOCKETS * NUM_PACKET_BURSTS +# The tast passes if more than 75% of packets are received in each group. +PACKET_RECV_THRESH = 0.75 * TOTAL_PACKET_BURSTS +PORT = 8000 + + +# Server +def instance0(): + recv_count = {i: 0 for i in range(NUM_PACKET_GROUPS)} + multitest.globals(IP=multitest.get_network_ip()) + multitest.next() + for i in range(NUM_NEW_SOCKETS): + print("test socket", i) + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(socket.getaddrinfo("0.0.0.0", PORT + i)[0][-1]) + s.settimeout(0.250) + multitest.broadcast("server ready") + for burst in range(NUM_PACKET_BURSTS): + # Wait for all packets to be sent, without receiving any yet. + multitest.wait("data sent burst={}".format(burst)) + # Try to receive all packets (they should be waiting in the queue). + for group in range(NUM_PACKET_GROUPS): + try: + data, addr = s.recvfrom(1000) + except: + continue + recv_burst, recv_group = data.split(b":") + recv_burst = int(recv_burst) + recv_group = int(recv_group) + if recv_burst == burst: + recv_count[recv_group] += 1 + # Inform the client that all data was received. + multitest.broadcast("data received burst={}".format(burst)) + s.close() + + # Check how many packets were received. + for group, count in recv_count.items(): + if count >= PACKET_RECV_THRESH: + print("pass group={}".format(group)) + else: + print("fail group={} received={}%".format(group, 100 * count // TOTAL_PACKET_BURSTS)) + + +# Client +def instance1(): + multitest.next() + for i in range(NUM_NEW_SOCKETS): + print("test socket", i) + ai = socket.getaddrinfo(IP, PORT + i)[0][-1] + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + multitest.wait("server ready") + for burst in range(NUM_PACKET_BURSTS): + # Send a bunch of packets all in a row. + for group in range(NUM_PACKET_GROUPS): + s.sendto(b"%d:%d" % (burst, group), ai) + # Inform the server that the data has been sent. + multitest.broadcast("data sent burst={}".format(burst)) + # Wait for the server to finish receiving. + multitest.wait("data received burst={}".format(burst)) + s.close() diff --git a/tests/multi_net/udp_data_multi.py.exp b/tests/multi_net/udp_data_multi.py.exp new file mode 100644 index 00000000000..a74ef42b730 --- /dev/null +++ b/tests/multi_net/udp_data_multi.py.exp @@ -0,0 +1,14 @@ +--- instance0 --- +test socket 0 +test socket 1 +test socket 2 +test socket 3 +pass group=0 +pass group=1 +pass group=2 +pass group=3 +--- instance1 --- +test socket 0 +test socket 1 +test socket 2 +test socket 3 diff --git a/tests/multi_net/udp_recv_dontwait.py b/tests/multi_net/udp_recv_dontwait.py new file mode 100644 index 00000000000..2d2ee572d34 --- /dev/null +++ b/tests/multi_net/udp_recv_dontwait.py @@ -0,0 +1,59 @@ +# Test UDP recv and recvfrom with MSG_DONTWAIT +import random +import socket + +try: + import errno, time +except ImportError: + print("SKIP") + raise SystemExit + + +# Server +def instance0(): + PORT = 10000 + random.getrandbits(15) + multitest.globals(IP=multitest.get_network_ip(), PORT=PORT) + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1]) + multitest.next() + begin = time.ticks_ms() + + # do some recvs before instance1 starts, when we know no packet is waiting + try: + print(s.recvfrom(8, socket.MSG_DONTWAIT)) + except OSError as e: + print(e.errno == errno.EAGAIN) + try: + print(s.recv(8, socket.MSG_DONTWAIT)) + except OSError as e: + print(e.errno == errno.EAGAIN) + + # the above steps should not have taken any substantial time + elapsed = time.ticks_diff(time.ticks_ms(), begin) + print(True if elapsed < 50 else elapsed) + + # Now instance1 will send us a UDP packet + multitest.broadcast("0-ready") + multitest.wait("1-sent") + + for _ in range(10): # retry if necessary, to allow for network delay + time.sleep_ms(100) + try: + print(s.recv(8, socket.MSG_DONTWAIT)) + break + except OSError as er: + if er.errno != errno.EAGAIN: + raise er + s.close() + + +# Client +def instance1(): + multitest.next() + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(socket.getaddrinfo(IP, PORT)[0][-1]) + multitest.wait("0-ready") + print(s.send(b"abcdefgh")) + multitest.broadcast("1-sent") + s.close() diff --git a/tests/multi_net/udp_recv_dontwait.py.exp b/tests/multi_net/udp_recv_dontwait.py.exp new file mode 100644 index 00000000000..f61fd4bbe2e --- /dev/null +++ b/tests/multi_net/udp_recv_dontwait.py.exp @@ -0,0 +1,7 @@ +--- instance0 --- +True +True +True +b'abcdefgh' +--- instance1 --- +8 diff --git a/tests/multi_net/udp_recv_peek.py b/tests/multi_net/udp_recv_peek.py new file mode 100644 index 00000000000..e8a5f112919 --- /dev/null +++ b/tests/multi_net/udp_recv_peek.py @@ -0,0 +1,36 @@ +# Test UDP recv and recvfrom with MSG_PEEK +import random +import socket +import time + + +# Server +def instance0(): + PORT = 10000 + random.getrandbits(15) + multitest.globals(IP=multitest.get_network_ip(), PORT=PORT) + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(socket.getaddrinfo("0.0.0.0", PORT)[0][-1]) + multitest.next() + peek_bytes, peek_addr = s.recvfrom(8, socket.MSG_PEEK) + print(peek_bytes) + real_bytes, real_addr = s.recvfrom(8) + print(real_bytes) + print(peek_addr == real_addr) # source addr should be the same for each + res = s.sendto(b"1234567890", peek_addr) + print(res) + print(s.recv(5, socket.MSG_PEEK)) + print(s.recv(5, socket.MSG_PEEK)) + s.close() + + +# Client +def instance1(): + multitest.next() + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(socket.getaddrinfo(IP, PORT)[0][-1]) + s.send(b"abcdefgh") + s.send(b"klmnopqr") + print(s.recv(5, socket.MSG_PEEK)) + print(s.recv(10)) + s.close() diff --git a/tests/multi_pyb_can/rx_callback.py b/tests/multi_pyb_can/rx_callback.py new file mode 100644 index 00000000000..960a225c93f --- /dev/null +++ b/tests/multi_pyb_can/rx_callback.py @@ -0,0 +1,98 @@ +from pyb import CAN +import time +import errno + +# Test the various receive IRQs, including overflow + +rx_overflow = False + +REASONS = ["received", "full", "overflow"] + +# CAN IDs +ID_SPAM = 0x345 # messages spammed into the receive FIFO +ID_ACK_OFLOW = 0x055 # message the receiver sends after it's seen an overflow +ID_AFTER = 0x100 # message the sender sends after the ACK + + +def cb0(bus, reason): + global rx_overflow + if reason != 0 and not rx_overflow: + # exact timing of 'received' callbacks depends on controller type, + # so only log the other two + print("rx0 reason", REASONS[reason]) + if reason == 2: + rx_overflow = True + + +# Accept all standard IDs on FIFO 0 +def _enable_accept_all(): + if hasattr(CAN, "MASK"): # FD-CAN controller + can.setfilter(0, CAN.RANGE, 0, (0x0, 0x7FF), extframe=False) + else: + can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0), extframe=False) + + +# Receiver +def instance0(): + _enable_accept_all() + can.rxcallback(0, cb0) + + multitest.next() + multitest.wait("sender ready") + multitest.broadcast("receiver ready") + + while not rx_overflow: + pass # Resume ASAP after FIFO0 overflows + + can.send(b"overflow", ID_ACK_OFLOW) + + # drain the receive FIFO, making sure we read at least on ID_SPAM message + rxed_spam = False + while can.any(0): + msg = can.recv(0, timeout=0) + assert msg[0] == ID_SPAM + rxed_spam = True + print("rxed_spam", rxed_spam) + + # This should be the one message with ID_AFTER, there may be one or two spam messages as well + for _ in range(10): + msg = can.recv(0, timeout=500) + if msg[0] == ID_AFTER: + print(msg) + break + + # RX FIFO should be empty now + print("any", can.any(0)) + + +# Sender +def instance1(): + _enable_accept_all() + multitest.next() + multitest.broadcast("sender ready") + multitest.wait("receiver ready") + + # Spam out messages until the receiver tells us its RX FIFO is full. + # + # The RX FIFO on the receiver can vary from 3 deep (BXCAN) to 25 deep (STM32H7), + # so we keep sending to it until we see a CAN message on ID_ACK_OFLOW indicating + # the receiver's FIFO has overflowed + for i in range(255): + can.send(bytes([i] * 8), ID_SPAM, timeout=25) + if can.any(0): + print(can.recv(0)) # should be ID_ACK_OFLOW + break + # on boards like STM32H7 the TX FIFO is really deep, so don't fill it too quickly... + time.sleep_ms(1) + + # give the receiver some time to make space in the FIFO + time.sleep_ms(200) + + # send the final message, the receiver should get this one + can.send(b"aaaaa", ID_AFTER) + + # Sender's RX FIFO should also be empty at this point + print("any", can.any(0)) + + +can = CAN(1, CAN.NORMAL, baudrate=500_000, sample_point=75) diff --git a/tests/multi_pyb_can/rx_callback.py.exp b/tests/multi_pyb_can/rx_callback.py.exp new file mode 100644 index 00000000000..2126a3fccb0 --- /dev/null +++ b/tests/multi_pyb_can/rx_callback.py.exp @@ -0,0 +1,9 @@ +--- instance0 --- +rx0 reason full +rx0 reason overflow +rxed_spam True +[256, False, False, 0, b'aaaaa'] +any False +--- instance1 --- +[85, False, False, 0, b'overflow'] +any False diff --git a/tests/multi_pyb_can/rx_filters.py b/tests/multi_pyb_can/rx_filters.py new file mode 100644 index 00000000000..22e5eb03647 --- /dev/null +++ b/tests/multi_pyb_can/rx_filters.py @@ -0,0 +1,50 @@ +from pyb import CAN +import time +import errno + +# Test for the filtering capabilities for RX FIFO 0 and 1. + + +# Receiver +def instance0(): + # Configure to receive standard frames (in a range) on FIFO 0 + # and extended frames (in a range) on FIFO 1. + if hasattr(CAN, "MASK"): + # FD-CAN has independent filter banks for standard and extended IDs + can.setfilter(0, CAN.MASK, 0, (0x300, 0x700), extframe=False) + can.setfilter(0, CAN.MASK, 1, (0x3000, 0x7000), extframe=True) + else: + # pyb.CAN only supports MASK32 for extended ids + can.setfilter(0, CAN.MASK16, 0, (0x300, 0x700, 0x300, 0x700), extframe=False) + can.setfilter(1, CAN.MASK32, 1, (0x3000, 0x7000), extframe=True) + + multitest.next() + multitest.wait("sender ready") + multitest.broadcast("receiver ready") + for i in range(3): + print(i) + print("fifo0", can.recv(0, timeout=200)) + print("fifo1", can.recv(1, timeout=200)) + + try: + can.recv(0, timeout=100) # should time out + except OSError as e: + assert e.errno == errno.ETIMEDOUT + print("Timed out as expected") + + +# Sender +def instance1(): + multitest.next() + multitest.broadcast("sender ready") + multitest.wait("receiver ready") + + for i in range(3): + print(i) + can.send(bytes([i, 3] * i), 0x345) + can.send(bytes([0xEE] * i), 0x3700 + i, extframe=True) + can.send(b"abcdef", 0x123) # matches no filter, expect ACKed but not received + time.sleep_ms(5) # avoid flooding either our or the receiver's FIFO + + +can = CAN(1, CAN.NORMAL, baudrate=500_000, sample_point=75) diff --git a/tests/multi_pyb_can/rx_filters.py.exp b/tests/multi_pyb_can/rx_filters.py.exp new file mode 100644 index 00000000000..8016f641524 --- /dev/null +++ b/tests/multi_pyb_can/rx_filters.py.exp @@ -0,0 +1,15 @@ +--- instance0 --- +0 +fifo0 [837, False, False, 0, b''] +fifo1 [14080, True, False, 0, b''] +1 +fifo0 [837, False, False, 0, b'\x01\x03'] +fifo1 [14081, True, False, 0, b'\xee'] +2 +fifo0 [837, False, False, 0, b'\x02\x03\x02\x03'] +fifo1 [14082, True, False, 0, b'\xee\xee'] +Timed out as expected +--- instance1 --- +0 +1 +2 diff --git a/tests/multi_wlan/01_ap_sta.py b/tests/multi_wlan/01_ap_sta.py new file mode 100644 index 00000000000..368addf1393 --- /dev/null +++ b/tests/multi_wlan/01_ap_sta.py @@ -0,0 +1,109 @@ +# Basic Wi-Fi MAC layer test where one device creates an Access Point and the +# other device connects to it as a Station. Also tests channel assignment (where +# possible) and disconnection. + +try: + import network + + network.WLAN +except (ImportError, NameError): + print("SKIP") + raise SystemExit + +import os +import sys +import time + +CHANNEL = 8 + +# Note that on slower Wi-Fi stacks this bumps up against the run-multitests.py +# timeout which expects <10s between lines of output. We work around this by +# logging something half way through the wait_for loop... +CONNECT_TIMEOUT = 15000 + + +def wait_for(test_func): + has_printed = False + start = time.ticks_ms() + while not test_func(): + time.sleep(0.1) + delta = time.ticks_diff(time.ticks_ms(), start) + if not has_printed and delta > CONNECT_TIMEOUT / 2: + print("...") + has_printed = True + elif delta > CONNECT_TIMEOUT: + break + + if not has_printed: + print("...") # keep the output consistent + + return test_func() + + +# AP +def instance0(): + ap = network.WLAN(network.WLAN.IF_AP) + ssid = "MP-test-" + os.urandom(6).hex() + psk = "Secret-" + os.urandom(6).hex() + + # stop any previous activity + network.WLAN(network.WLAN.IF_STA).active(False) + ap.active(False) + + ap.active(True) + ap.config(ssid=ssid, key=psk, channel=CHANNEL, security=network.WLAN.SEC_WPA_WPA2) + + # print("AP setup", ssid, psk) + print("AP started") + + multitest.globals(SSID=ssid, PSK=psk) + multitest.next() + + # Wait for station + if not wait_for(ap.isconnected): + raise RuntimeError("Timed out waiting for station, status ", ap.status()) + + print("AP got station") + time.sleep( + 3 + ) # depending on port, may still need to negotiate DHCP lease for STA to see connection + + print("AP disabling...") + ap.active(False) + + +# STA +def instance1(): + sta = network.WLAN(network.WLAN.IF_STA) + + # stop any previous activity + network.WLAN(network.WLAN.IF_AP).active(False) + sta.active(False) + + multitest.next() + ssid = SSID + psk = PSK + + # print("STA setup", ssid, psk) + + sta.active(True) + sta.connect(ssid, psk) + + print("STA connecting...") + + if not wait_for(sta.isconnected): + raise RuntimeError("Timed out waiting to connect, status ", sta.status()) + + print("STA connected") + + print("channel", sta.config("channel")) + + print("STA waiting for disconnect...") + + # Expect the AP to disconnect us immediately + if not wait_for(lambda: not sta.isconnected()): + raise RuntimeError("Timed out waiting for AP to disconnect us, status ", sta.status()) + + print("STA disconnected") + + sta.active(False) diff --git a/tests/multi_wlan/01_ap_sta.py.exp b/tests/multi_wlan/01_ap_sta.py.exp new file mode 100644 index 00000000000..8fc023a3980 --- /dev/null +++ b/tests/multi_wlan/01_ap_sta.py.exp @@ -0,0 +1,13 @@ +--- instance0 --- +AP started +... +AP got station +AP disabling... +--- instance1 --- +STA connecting... +... +STA connected +channel 8 +STA waiting for disconnect... +... +STA disconnected diff --git a/tests/multi_wlan/getaddrinfo.py b/tests/multi_wlan/getaddrinfo.py new file mode 100644 index 00000000000..8ad6155ed0e --- /dev/null +++ b/tests/multi_wlan/getaddrinfo.py @@ -0,0 +1,42 @@ +# This is a regression test to ensure getaddrinfo() fails (after a timeout) +# when Wi-Fi is disconnected. +# +# It doesn't require multiple instances, but it does require Wi-Fi to be present +# but not active, and for no other network interface to be configured. The only +# tests which already meet these conditions is here in multi_wlan tests. +try: + from network import WLAN +except (ImportError, NameError): + print("SKIP") + raise SystemExit + +import socket + + +def instance0(): + WLAN(WLAN.IF_AP).active(0) + wlan = WLAN(WLAN.IF_STA) + wlan.active(0) + + multitest.next() + + # Note: Lookup domain for this test doesn't need to exist, as the board + # isn't internet connected. It also shouldn't exist, so a cached result is + # never returned! + try: + socket.getaddrinfo("doesnotexist.example.com", 80) + except OSError as er: + print( + "active(0) failed" + ) # This may fail with code -6 or -2 depending on whether WLAN has been active since reset + + wlan.active(1) + + try: + socket.getaddrinfo("doesnotexist.example.com", 80) + except OSError as er: + print( + "active(1) failed", er.errno in (-2, -202) + ) # This one should always be -2 or -202 depending on port + + wlan.active(0) diff --git a/tests/multi_wlan/getaddrinfo.py.exp b/tests/multi_wlan/getaddrinfo.py.exp new file mode 100644 index 00000000000..5616a23c728 --- /dev/null +++ b/tests/multi_wlan/getaddrinfo.py.exp @@ -0,0 +1,3 @@ +--- instance0 --- +active(0) failed +active(1) failed True diff --git a/tests/net_hosted/asyncio_loopback.py b/tests/net_hosted/asyncio_loopback.py index fd4674544ce..03513ae6244 100644 --- a/tests/net_hosted/asyncio_loopback.py +++ b/tests/net_hosted/asyncio_loopback.py @@ -1,5 +1,12 @@ # Test network loopback behaviour +import sys + +# Only certain platforms can do TCP/IP loopback. +if sys.platform not in ("darwin", "esp32", "linux"): + print("SKIP") + raise SystemExit + try: import asyncio except ImportError: diff --git a/tests/net_hosted/connect_nonblock_xfer.py b/tests/net_hosted/connect_nonblock_xfer.py index dc4693cea6a..2c8e12473fe 100644 --- a/tests/net_hosted/connect_nonblock_xfer.py +++ b/tests/net_hosted/connect_nonblock_xfer.py @@ -1,145 +1,135 @@ # test that socket.connect() on a non-blocking socket raises EINPROGRESS # and that an immediate write/send/read/recv does the right thing -import sys, time, socket, errno, ssl +import unittest +import errno +import select +import socket +import ssl -isMP = sys.implementation.name == "micropython" +# only mbedTLS supports non-blocking mode +ssl_supports_nonblocking = hasattr(ssl, "MBEDTLS_VERSION") -def dp(e): - # uncomment next line for development and testing, to print the actual exceptions - # print(repr(e)) - pass +# get the name of an errno error code +def errno_name(er): + if er == errno.EAGAIN: + return "EAGAIN" + if er == errno.EINPROGRESS: + return "EINPROGRESS" + return er # do_connect establishes the socket and wraps it if tls is True. # If handshake is true, the initial connect (and TLS handshake) is # allowed to be performed before returning. -def do_connect(peer_addr, tls, handshake): +def do_connect(self, peer_addr, tls, handshake): s = socket.socket() s.setblocking(False) try: - # print("Connecting to", peer_addr) + print("Connecting to", peer_addr) s.connect(peer_addr) + self.fail() except OSError as er: - print("connect:", er.errno == errno.EINPROGRESS) - if er.errno != errno.EINPROGRESS: - print(" got", er.errno) + print("connect:", errno_name(er.errno)) + self.assertEqual(er.errno, errno.EINPROGRESS) + # wrap with ssl/tls if desired if tls: + print("wrap socket") ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - if hasattr(ssl_context, "check_hostname"): - ssl_context.check_hostname = False - - try: - s = ssl_context.wrap_socket(s, do_handshake_on_connect=handshake) - print("wrap: True") - except Exception as e: - dp(e) - print("wrap:", e) - elif handshake: - # just sleep a little bit, this allows any connect() errors to happen - time.sleep(0.2) + s = ssl_context.wrap_socket(s, do_handshake_on_connect=handshake) + return s -# test runs the test against a specific peer address. -def test(peer_addr, tls=False, handshake=False): - # MicroPython plain sockets have read/write, but CPython's don't - # MicroPython TLS sockets and CPython's have read/write - # hasRW captures this wonderful state of affairs - hasRW = isMP or tls +# poll a socket and check the result +def poll(self, s, expect_writable): + poller = select.poll() + poller.register(s) + result = poller.poll(0) + print("poll:", result) + if expect_writable: + self.assertEqual(len(result), 1) + self.assertEqual(result[0][1], select.POLLOUT) + else: + self.assertEqual(result, []) + - # MicroPython plain sockets and CPython's have send/recv - # MicroPython TLS sockets don't have send/recv, but CPython's do - # hasSR captures this wonderful state of affairs - hasSR = not (isMP and tls) +# do_test runs the test against a specific peer address. +def do_test(self, peer_addr, tls, handshake): + print() + + # MicroPython plain and TLS sockets have read/write + hasRW = True + + # MicroPython plain sockets have send/recv + # MicroPython TLS sockets don't have send/recv + hasSR = not tls # connect + send + # non-blocking send should raise EAGAIN if hasSR: - s = do_connect(peer_addr, tls, handshake) - # send -> 4 or EAGAIN - try: + s = do_connect(self, peer_addr, tls, handshake) + poll(self, s, False) + with self.assertRaises(OSError) as ctx: ret = s.send(b"1234") - print("send:", handshake and ret == 4) - except OSError as er: - # - dp(er) - print("send:", er.errno in (errno.EAGAIN, errno.EINPROGRESS)) + print("send error:", errno_name(ctx.exception.errno)) + self.assertEqual(ctx.exception.errno, errno.EAGAIN) s.close() - else: # fake it... - print("connect:", True) - if tls: - print("wrap:", True) - print("send:", True) # connect + write + # non-blocking write should return None if hasRW: - s = do_connect(peer_addr, tls, handshake) - # write -> None - try: - ret = s.write(b"1234") - print("write:", ret in (4, None)) # SSL may accept 4 into buffer - except OSError as er: - dp(er) - print("write:", False) # should not raise - except ValueError as er: # CPython - dp(er) - print("write:", er.args[0] == "Write on closed or unwrapped SSL socket.") + s = do_connect(self, peer_addr, tls, handshake) + poll(self, s, tls and handshake) + ret = s.write(b"1234") + print("write:", ret) + if tls and handshake: + self.assertEqual(ret, 4) + else: + self.assertIsNone(ret) s.close() - else: # fake it... - print("connect:", True) - if tls: - print("wrap:", True) - print("write:", True) + # connect + recv + # non-blocking recv should raise EAGAIN if hasSR: - # connect + recv - s = do_connect(peer_addr, tls, handshake) - # recv -> EAGAIN - try: - print("recv:", s.recv(10)) - except OSError as er: - dp(er) - print("recv:", er.errno == errno.EAGAIN) + s = do_connect(self, peer_addr, tls, handshake) + poll(self, s, False) + with self.assertRaises(OSError) as ctx: + ret = s.recv(10) + print("recv error:", errno_name(ctx.exception.errno)) + self.assertEqual(ctx.exception.errno, errno.EAGAIN) s.close() - else: # fake it... - print("connect:", True) - if tls: - print("wrap:", True) - print("recv:", True) # connect + read + # non-blocking read should return None if hasRW: - s = do_connect(peer_addr, tls, handshake) - # read -> None - try: - ret = s.read(10) - print("read:", ret is None) - except OSError as er: - dp(er) - print("read:", False) # should not raise - except ValueError as er: # CPython - dp(er) - print("read:", er.args[0] == "Read on closed or unwrapped SSL socket.") + s = do_connect(self, peer_addr, tls, handshake) + poll(self, s, tls and handshake) + ret = s.read(10) + print("read:", ret) + self.assertIsNone(ret) s.close() - else: # fake it... - print("connect:", True) - if tls: - print("wrap:", True) - print("read:", True) -if __name__ == "__main__": +class Test(unittest.TestCase): # these tests use a non-existent test IP address, this way the connect takes forever and # we can see EAGAIN/None (https://tools.ietf.org/html/rfc5737) - print("--- Plain sockets to nowhere ---") - test(socket.getaddrinfo("192.0.2.1", 80)[0][-1], False, False) - print("--- SSL sockets to nowhere ---") - # this test fails with AXTLS because do_handshake=False blocks on first read/write and - # there it times out until the connect is aborted - test(socket.getaddrinfo("192.0.2.1", 443)[0][-1], True, False) - print("--- Plain sockets ---") - test(socket.getaddrinfo("micropython.org", 80)[0][-1], False, True) - print("--- SSL sockets ---") - test(socket.getaddrinfo("micropython.org", 443)[0][-1], True, True) + def test_plain_sockets_to_nowhere(self): + do_test(self, socket.getaddrinfo("192.0.2.1", 80)[0][-1], False, False) + + @unittest.skipIf(not ssl_supports_nonblocking, "SSL doesn't support non-blocking") + def test_ssl_sockets_to_nowhere(self): + do_test(self, socket.getaddrinfo("192.0.2.1", 443)[0][-1], True, False) + + def test_plain_sockets(self): + do_test(self, socket.getaddrinfo("micropython.org", 80)[0][-1], False, False) + + @unittest.skipIf(not ssl_supports_nonblocking, "SSL doesn't support non-blocking") + def test_ssl_sockets(self): + do_test(self, socket.getaddrinfo("micropython.org", 443)[0][-1], True, True) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/net_hosted/ssl_verify_callback.py b/tests/net_hosted/ssl_verify_callback.py index 0dba4e4fddc..cd03ff49d77 100644 --- a/tests/net_hosted/ssl_verify_callback.py +++ b/tests/net_hosted/ssl_verify_callback.py @@ -4,6 +4,12 @@ import socket import tls +context = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT) + +if not hasattr(context, "verify_callback"): + print("SKIP") + raise SystemExit + def verify_callback(cert, depth): print("verify_callback:", type(cert), len(cert) > 100, depth) @@ -16,7 +22,6 @@ def verify_callback_fail(cert, depth): def test(peer_addr): - context = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT) context.verify_mode = tls.CERT_OPTIONAL context.verify_callback = verify_callback s = socket.socket() diff --git a/tests/net_inet/asyncio_tls_open_connection_readline.py b/tests/net_inet/asyncio_tls_open_connection_readline.py index 70145d91a79..53b7705d27a 100644 --- a/tests/net_inet/asyncio_tls_open_connection_readline.py +++ b/tests/net_inet/asyncio_tls_open_connection_readline.py @@ -2,31 +2,14 @@ import os import asyncio -# This certificate was obtained from micropython.org using openssl: -# $ openssl s_client -showcerts -connect micropython.org:443 /dev/null -# The certificate is from Let's Encrypt: -# 1 s:/C=US/O=Let's Encrypt/CN=R3 -# i:/C=US/O=Internet Security Research Group/CN=ISRG Root X1 -# Validity -# Not Before: Sep 4 00:00:00 2020 GMT -# Not After : Sep 15 16:00:00 2025 GMT -# Copy PEM content to a file (certmpy.pem) and convert to DER e.g. -# $ openssl x509 -in certmpy.pem -out certmpy.der -outform DER -# Then convert to hex format, eg using binascii.hexlify(data). - -# Note that the instructions above is to obtain an intermediate -# root CA cert that works for MicroPython. However CPython needs the ultimate root CA -# cert from ISRG - -ca_cert_chain = "isrg.der" - -try: - os.stat(ca_cert_chain) -except OSError: +if not hasattr(ssl, "CERT_REQUIRED"): print("SKIP") raise SystemExit -with open(ca_cert_chain, "rb") as ca: +# For details about this cert, see comment in test_sslcontext_client.py +root_cert_path = "isrgrootx1.der" + +with open(root_cert_path, "rb") as ca: cadata = ca.read() client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) diff --git a/tests/net_inet/isrg.der b/tests/net_inet/isrgrootx1.der similarity index 100% rename from tests/net_inet/isrg.der rename to tests/net_inet/isrgrootx1.der diff --git a/tests/net_inet/mpycert.der b/tests/net_inet/mpycert.der deleted file mode 100644 index 0b0eabc9bc8..00000000000 Binary files a/tests/net_inet/mpycert.der and /dev/null differ diff --git a/tests/net_inet/resolve_on_connect.py b/tests/net_inet/resolve_on_connect.py new file mode 100644 index 00000000000..ac36e0f7fcd --- /dev/null +++ b/tests/net_inet/resolve_on_connect.py @@ -0,0 +1,38 @@ +# Test that the socket module performs DNS resolutions on bind and connect. +# Currenty only the esp32 port does this, so the test is restricted to that port. + +import sys + +if sys.implementation.name == "micropython" and sys.platform != "esp32": + print("SKIP") + raise SystemExit + +import socket +import unittest + + +class Test(unittest.TestCase): + def test_bind_resolves_0_0_0_0(self): + s = socket.socket() + self.assertEqual(s.bind(("0.0.0.0", 31245)), None) + s.close() + + def test_bind_resolves_localhost(self): + s = socket.socket() + self.assertEqual(s.bind(("localhost", 31245)), None) + s.close() + + def test_connect_resolves(self): + s = socket.socket() + self.assertEqual(s.connect(("micropython.org", 80)), None) + s.close() + + def test_connect_non_existent(self): + s = socket.socket() + with self.assertRaises(OSError): + s.connect(("nonexistent.example.com", 80)) + s.close() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/net_inet/ssl_cert.py b/tests/net_inet/ssl_cert.py index 3597b7855db..ad285dc7702 100644 --- a/tests/net_inet/ssl_cert.py +++ b/tests/net_inet/ssl_cert.py @@ -1,63 +1,23 @@ -import binascii import socket import ssl +if not hasattr(ssl, "CERT_REQUIRED"): + print("SKIP") + raise SystemExit -# This certificate was obtained from micropython.org using openssl: -# $ openssl s_client -showcerts -connect micropython.org:443 /dev/null -# The certificate is from Let's Encrypt: -# 1 s:C=US, O=Let's Encrypt, CN=R11 -# i:C=US, O=Internet Security Research Group, CN=ISRG Root X1 -# a:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256 -# v:NotBefore: Mar 13 00:00:00 2024 GMT; NotAfter: Mar 12 23:59:59 2027 GMT -# Copy PEM content to a file (certmpy.pem) and convert to DER e.g. -# $ openssl x509 -in certmpy.pem -out certmpy.der -outform DER -# Then convert to hex format, eg using binascii.hexlify(data). - -ca_cert_chain = binascii.unhexlify( - b"30820506308202eea0030201020211008a7d3e13d62f30ef2386bd29076b34f8300d06092a864886" - b"f70d01010b0500304f310b300906035504061302555331293027060355040a1320496e7465726e65" - b"742053656375726974792052657365617263682047726f7570311530130603550403130c49535247" - b"20526f6f74205831301e170d3234303331333030303030305a170d3237303331323233353935395a" - b"3033310b300906035504061302555331163014060355040a130d4c6574277320456e637279707431" - b"0c300a0603550403130352313130820122300d06092a864886f70d01010105000382010f00308201" - b"0a0282010100ba87bc5c1b0039cbca0acdd46710f9013ca54ea561cb26ca52fb1501b7b928f5281e" - b"ed27b324183967090c08ece03ab03b770ebdf3e53954410c4eae41d69974de51dbef7bff58bda8b7" - b"13f6de31d5f272c9726a0b8374959c4600641499f3b1d922d9cda892aa1c267a3ffeef58057b0895" - b"81db710f8efbe33109bb09be504d5f8f91763d5a9d9e83f2e9c466b3e106664348188065a037189a" - b"9b843297b1b2bdc4f815009d2788fbe26317966c9b27674bc4db285e69c279f0495ce02450e1c4bc" - b"a105ac7b406d00b4c2413fa758b82fc55c9ba5bb099ef1feebb08539fda80aef45c478eb652ac2cf" - b"5f3cdee35c4d1bf70b272baa0b4277534f796a1d87d90203010001a381f83081f5300e0603551d0f" - b"0101ff040403020186301d0603551d250416301406082b0601050507030206082b06010505070301" - b"30120603551d130101ff040830060101ff020100301d0603551d0e04160414c5cf46a4eaf4c3c07a" - b"6c95c42db05e922f26e3b9301f0603551d2304183016801479b459e67bb6e5e40173800888c81a58" - b"f6e99b6e303206082b0601050507010104263024302206082b060105050730028616687474703a2f" - b"2f78312e692e6c656e63722e6f72672f30130603551d20040c300a3008060667810c010201302706" - b"03551d1f0420301e301ca01aa0188616687474703a2f2f78312e632e6c656e63722e6f72672f300d" - b"06092a864886f70d01010b050003820201004ee2895d0a031c9038d0f51ff9715cf8c38fb237887a" - b"6fb0251fedbeb7d886068ee90984cd72bf81f3fccacf5348edbdf66942d4a5113e35c813b2921d05" - b"5fea2ed4d8f849c3adf599969cef26d8e1b4240b48204dfcd354b4a9c621c8e1361bff77642917b9" - b"f04bef5deacd79d0bf90bfbe23b290da4aa9483174a9440be1e2f62d8371a4757bd294c10519461c" - b"b98ff3c47448252a0de5f5db43e2db939bb919b41f2fdf6a0e8f31d3630fbb29dcdd662c3fb01b67" - b"51f8413ce44db9acb8a49c6663f5ab85231dcc53b6ab71aedcc50171da36ee0a182a32fd09317c8f" - b"f673e79c9cb54a156a77825acfda8d45fe1f2a6405303e73c2c60cb9d63b634aab4603fe99c04640" - b"276063df503a0747d8154a9fea471f995a08620cb66c33084dd738ed482d2e0568ae805def4cdcd8" - b"20415f68f1bb5acde30eb00c31879b43de4943e1c8043fd13c1b87453069a8a9720e79121c31d83e" - b"2357dda74fa0f01c81d1771f6fd6d2b9a8b3031681394b9f55aed26ae4b3bfeaa5d59f4ba3c9d63b" - b"72f34af654ab0cfc38f76080df6e35ca75a154e42fbc6e17c91aa537b5a29abaecf4c075464f77a8" - b"e8595691662d6ede2981d6a697055e6445be2cceea644244b0c34fadf0b4dc03ca999b098295820d" - b"638a66f91972f8d5b98910e289980935f9a21cbe92732374e99d1fd73b4a9a845810c2f3a7e235ec" - b"7e3b45ce3046526bc0c0" -) +# For details about this cert, see comment in test_sslcontext_client.py +root_cert_path = "isrgrootx1.der" def main(use_stream=True): + with open(root_cert_path, "rb") as f: + cadata = f.read() s = socket.socket() ai = socket.getaddrinfo("micropython.org", 443) addr = ai[0][-1] s.connect(addr) s = ssl.wrap_socket( - s, cert_reqs=ssl.CERT_REQUIRED, cadata=ca_cert_chain, server_hostname="micropython.org" + s, cert_reqs=ssl.CERT_REQUIRED, cadata=cadata, server_hostname="micropython.org" ) s.write(b"GET / HTTP/1.0\r\n\r\n") print(s.read(17)) diff --git a/tests/net_inet/ssl_errors.py b/tests/net_inet/ssl_errors.py index bc4e5910bcc..c23f736b09b 100644 --- a/tests/net_inet/ssl_errors.py +++ b/tests/net_inet/ssl_errors.py @@ -3,6 +3,10 @@ import sys, errno, select, socket, ssl +if not hasattr(ssl, "CERT_REQUIRED"): + print("SKIP") + raise SystemExit + def test(addr, hostname, block=True): print("---", hostname) diff --git a/tests/net_inet/test_sslcontext_client.py b/tests/net_inet/test_sslcontext_client.py index 30ec0ac7c83..5aa00edf9d3 100644 --- a/tests/net_inet/test_sslcontext_client.py +++ b/tests/net_inet/test_sslcontext_client.py @@ -2,25 +2,16 @@ import socket import ssl -# This certificate was obtained from micropython.org using openssl: -# $ openssl s_client -showcerts -connect micropython.org:443 /dev/null -# The certificate is from Let's Encrypt: -# 1 s:C=US, O=Let's Encrypt, CN=R11 -# i:C=US, O=Internet Security Research Group, CN=ISRG Root X1 -# a:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256 -# v:NotBefore: Mar 13 00:00:00 2024 GMT; NotAfter: Mar 12 23:59:59 2027 GMT -# Copy PEM content to a file (certmpy.pem) and convert to DER e.g. -# $ openssl x509 -in certmpy.pem -out certmpy.der -outform DER -# Then convert to hex format, eg using binascii.hexlify(data). - - -ca_cert_chain = "mpycert.der" -try: - os.stat(ca_cert_chain) -except OSError: +if not hasattr(ssl, "CERT_REQUIRED"): print("SKIP") raise SystemExit +# This ISRG Root X1 certificate was downloaded from https://letsencrypt.org/certificates/#root-cas +# This cert is used to sign the intermediate cert in use by micropython.org +# To check the current intermediate cert, can run: +# $ openssl s_client -showcerts -connect micropython.org:443 /dev/null +root_cert_path = "isrgrootx1.der" + def main(use_stream=True): context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) @@ -28,7 +19,7 @@ def main(use_stream=True): context.verify_mode = ssl.CERT_REQUIRED assert context.verify_mode == ssl.CERT_REQUIRED - context.load_verify_locations(cafile=ca_cert_chain) + context.load_verify_locations(cafile=root_cert_path) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) addr = socket.getaddrinfo("micropython.org", 443)[0][-1] diff --git a/tests/net_inet/test_tls_nonblock.py b/tests/net_inet/test_tls_nonblock.py index 60af858b1f1..3f9a61413f9 100644 --- a/tests/net_inet/test_tls_nonblock.py +++ b/tests/net_inet/test_tls_nonblock.py @@ -1,5 +1,12 @@ import socket, ssl, errno, sys, time, select +# Although this test doesn't need ssl.CERT_REQUIRED, it does require the ssl module +# to support modern ciphers. So exclude the test on axTLS which doesn't have +# CERT_REQUIRED. +if not hasattr(ssl, "CERT_REQUIRED"): + print("SKIP") + raise SystemExit + def test_one(site, opts): ai = socket.getaddrinfo(site, 443, socket.AF_INET) diff --git a/tests/net_inet/tls_num_errors.py b/tests/net_inet/tls_num_errors.py index 34aa2bb4551..79678e07274 100644 --- a/tests/net_inet/tls_num_errors.py +++ b/tests/net_inet/tls_num_errors.py @@ -1,22 +1,35 @@ # test that modtls produces a numerical error message when out of heap -import socket, ssl, sys +import socket, tls + +# This test is specific to the mbedTLS implementation, so require that. +if not hasattr(tls, "MBEDTLS_VERSION"): + print("SKIP") + raise SystemExit try: - from micropython import alloc_emergency_exception_buf, heap_lock, heap_unlock + from micropython import heap_lock, heap_unlock except: print("SKIP") raise SystemExit +try: + from micropython import alloc_emergency_exception_buf + + alloc_emergency_exception_buf(256) +except: + pass + # test with heap locked to see it switch to number-only error message def test(addr): - alloc_emergency_exception_buf(256) + ctx = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT) + ctx.verify_mode = tls.CERT_NONE s = socket.socket() s.connect(addr) try: s.setblocking(False) - s = ssl.wrap_socket(s, do_handshake=False) + s = ctx.wrap_socket(s, do_handshake_on_connect=False) heap_lock() print("heap is locked") while True: diff --git a/tests/net_inet/tls_text_errors.py.exp b/tests/net_inet/tls_text_errors.py.exp new file mode 100644 index 00000000000..2cedeb7c9a3 --- /dev/null +++ b/tests/net_inet/tls_text_errors.py.exp @@ -0,0 +1,2 @@ +######## +wrap: True diff --git a/tests/perf_bench/bm_fft.py b/tests/perf_bench/bm_fft.py index 9a2d03d11b9..e35c1216c13 100644 --- a/tests/perf_bench/bm_fft.py +++ b/tests/perf_bench/bm_fft.py @@ -15,7 +15,7 @@ def reverse(x, bits): # Initialization n = len(vector) - levels = int(math.log(n) / math.log(2)) + levels = int(round(math.log(n) / math.log(2))) coef = (2 if inverse else -2) * cmath.pi / n exptable = [cmath.rect(1, i * coef) for i in range(n // 2)] vector = [vector[reverse(i, levels)] for i in range(n)] # Copy with bit-reversed permutation diff --git a/tests/perf_bench/bm_pidigits.py b/tests/perf_bench/bm_pidigits.py index bdaa73cec7e..c935f103c5b 100644 --- a/tests/perf_bench/bm_pidigits.py +++ b/tests/perf_bench/bm_pidigits.py @@ -5,6 +5,12 @@ # This benchmark stresses big integer arithmetic. # Adapted from code on: http://benchmarksgame.alioth.debian.org/ +try: + int("0x10000000000000000", 16) +except: + print("SKIP") # No support for >64-bit integers + raise SystemExit + def compose(a, b): aq, ar, as_, at = a diff --git a/tests/perf_bench/core_import_mpy_multi.py b/tests/perf_bench/core_import_mpy_multi.py index 33437f9da80..55aa379e2bf 100644 --- a/tests/perf_bench/core_import_mpy_multi.py +++ b/tests/perf_bench/core_import_mpy_multi.py @@ -1,12 +1,13 @@ # Test performance of importing an .mpy file many times. -import sys, io, vfs - -if not hasattr(io, "IOBase"): +try: + import sys, vfs + from io import IOBase +except ImportError: print("SKIP") raise SystemExit -# This is the test.py file that is compiled to test.mpy below. +# This is the test.py file that is compiled to test.mpy below. mpy-cross must be invoked with `-msmall-int-bits=30`. """ class A: def __init__(self, arg): @@ -23,15 +24,17 @@ def f(): x = ("const tuple", None, False, True, 1, 2, 3) result = 123 """ -file_data = b'M\x06\x00\x1f\x14\x03\x0etest.py\x00\x0f\x02A\x00\x02f\x00\x0cresult\x00/-5#\x82I\x81{\x81w\x82/\x81\x05\x81\x17Iom\x82\x13\x06arg\x00\x05\x1cthis will be a string object\x00\x06\x1bthis will be a bytes object\x00\n\x07\x05\x0bconst tuple\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\x81\\\x10\n\x01\x89\x07d`T2\x00\x10\x024\x02\x16\x022\x01\x16\x03"\x80{\x16\x04Qc\x02\x81d\x00\x08\x02(DD\x11\x05\x16\x06\x10\x02\x16\x072\x00\x16\x082\x01\x16\t2\x02\x16\nQc\x03`\x1a\x08\x08\x12\x13@\xb1\xb0\x18\x13Qc@\t\x08\t\x12` Qc@\t\x08\n\x12``Qc\x82@ \x0e\x03\x80\x08+)##\x12\x0b\x12\x0c\x12\r\x12\x0e*\x04Y\x12\x0f\x12\x10\x12\x11*\x03Y#\x00\xc0#\x01\xc0#\x02\xc0Qc' +file_data = b'M\x06\x00\x1e\x14\x03\x0etest.py\x00\x0f\x02A\x00\x02f\x00\x0cresult\x00/-5#\x82I\x81{\x81w\x82/\x81\x05\x81\x17Iom\x82\x13\x06arg\x00\x05\x1cthis will be a string object\x00\x06\x1bthis will be a bytes object\x00\n\x07\x05\x0bconst tuple\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\x81\\\x10\n\x01\x89\x07d`T2\x00\x10\x024\x02\x16\x022\x01\x16\x03"\x80{\x16\x04Qc\x02\x81d\x00\x08\x02(DD\x11\x05\x16\x06\x10\x02\x16\x072\x00\x16\x082\x01\x16\t2\x02\x16\nQc\x03`\x1a\x08\x08\x12\x13@\xb1\xb0\x18\x13Qc@\t\x08\t\x12` Qc@\t\x08\n\x12``Qc\x82@ \x0e\x03\x80\x08+)##\x12\x0b\x12\x0c\x12\r\x12\x0e*\x04Y\x12\x0f\x12\x10\x12\x11*\x03Y#\x00\xc0#\x01\xc0#\x02\xc0Qc' -class File(io.IOBase): +class File(IOBase): def __init__(self): self.off = 0 def ioctl(self, request, arg): - return 0 + if request == 4: # MP_STREAM_CLOSE + return 0 + return -1 def readinto(self, buf): buf[:] = memoryview(file_data)[self.off : self.off + len(buf)] diff --git a/tests/perf_bench/core_import_mpy_single.py b/tests/perf_bench/core_import_mpy_single.py index 18454b8fd5e..755e4159487 100644 --- a/tests/perf_bench/core_import_mpy_single.py +++ b/tests/perf_bench/core_import_mpy_single.py @@ -2,13 +2,14 @@ # The first import of a module will intern strings that don't already exist, and # this test should be representative of what happens in a real application. -import sys, io, vfs - -if not hasattr(io, "IOBase"): +try: + import sys, vfs + from io import IOBase +except ImportError: print("SKIP") raise SystemExit -# This is the test.py file that is compiled to test.mpy below. +# This is the test.py file that is compiled to test.mpy below. mpy-cross must be invoked with `-msmall-int-bits=30`. # Many known and unknown names/strings are included to test the linking process. """ class A0: @@ -78,15 +79,17 @@ def f1(): x = ("const tuple 9", None, False, True, 1, 2, 3) result = 123 """ -file_data = b"M\x06\x00\x1f\x81=\x1e\x0etest.py\x00\x0f\x04A0\x00\x04A1\x00\x04f0\x00\x04f1\x00\x0cresult\x00/-5\x04a0\x00\x04a1\x00\x04a2\x00\x04a3\x00\x13\x15\x17\x19\x1b\x1d\x1f!#%')+1379;=?ACEGIKMOQSUWY[]_acegikmoqsuwy{}\x7f\x81\x01\x81\x03\x81\x05\x81\x07\x81\t\x81\x0b\x81\r\x81\x0f\x81\x11\x81\x13\x81\x15\x81\x17\x81\x19\x81\x1b\x81\x1d\x81\x1f\x81!\x81#\x81%\x81'\x81)\x81+\x81-\x81/\x811\x813\x815\x817\x819\x81;\x81=\x81?\x81A\x81C\x81E\x81G\x81I\x81K\x81M\x81O\x81Q\x81S\x81U\x81W\x81Y\x81[\x81]\x81_\x81a\x81c\x81e\x81g\x81i\x81k\x81m\x81o\x81q\x81s\x81u\x81w\x81y\x81{\x81}\x81\x7f\x82\x01\x82\x03\x82\x05\x82\x07\x82\t\x82\x0b\x82\r\x82\x0f\x82\x11\x82\x13\x82\x15\x82\x17\x82\x19\x82\x1b\x82\x1d\x82\x1f\x82!\x82#\x82%\x82'\x82)\x82+\x82-\x82/\x821\x823\x825\x827\x829\x82;\x82=\x82?\x82A\x82E\x82G\x82I\x82K\nname0\x00\nname1\x00\nname2\x00\nname3\x00\nname4\x00\nname5\x00\nname6\x00\nname7\x00\nname8\x00\nname9\x00$quite_a_long_name0\x00$quite_a_long_name1\x00$quite_a_long_name2\x00$quite_a_long_name3\x00$quite_a_long_name4\x00$quite_a_long_name5\x00$quite_a_long_name6\x00$quite_a_long_name7\x00$quite_a_long_name8\x00$quite_a_long_name9\x00&quite_a_long_name10\x00&quite_a_long_name11\x00\x05\x1ethis will be a string object 0\x00\x05\x1ethis will be a string object 1\x00\x05\x1ethis will be a string object 2\x00\x05\x1ethis will be a string object 3\x00\x05\x1ethis will be a string object 4\x00\x05\x1ethis will be a string object 5\x00\x05\x1ethis will be a string object 6\x00\x05\x1ethis will be a string object 7\x00\x05\x1ethis will be a string object 8\x00\x05\x1ethis will be a string object 9\x00\x06\x1dthis will be a bytes object 0\x00\x06\x1dthis will be a bytes object 1\x00\x06\x1dthis will be a bytes object 2\x00\x06\x1dthis will be a bytes object 3\x00\x06\x1dthis will be a bytes object 4\x00\x06\x1dthis will be a bytes object 5\x00\x06\x1dthis will be a bytes object 6\x00\x06\x1dthis will be a bytes object 7\x00\x06\x1dthis will be a bytes object 8\x00\x06\x1dthis will be a bytes object 9\x00\n\x07\x05\rconst tuple 0\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 1\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 2\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 3\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 4\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 5\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 6\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 7\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 8\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 9\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\x82d\x10\x12\x01i@i@\x84\x18\x84\x1fT2\x00\x10\x024\x02\x16\x02T2\x01\x10\x034\x02\x16\x032\x02\x16\x042\x03\x16\x05\"\x80{\x16\x06Qc\x04\x82\x0c\x00\n\x02($$$\x11\x07\x16\x08\x10\x02\x16\t2\x00\x16\n2\x01\x16\x0b2\x02\x16\x0c2\x03\x16\rQc\x04@\t\x08\n\x81\x0b Qc@\t\x08\x0b\x81\x0b@Qc@\t\x08\x0c\x81\x0b`QcH\t\n\r\x81\x0b` Qc\x82\x14\x00\x0c\x03h`$$$\x11\x07\x16\x08\x10\x03\x16\t2\x00\x16\n2\x01\x16\x0b2\x02\x16\x0c2\x03\x16\rQc\x04H\t\n\n\x81\x0b``QcH\t\n\x0b\x81\x0b\x80\x07QcH\t\n\x0c\x81\x0b\x80\x08QcH\t\n\r\x81\x0b\x80\tQc\xa08P:\x04\x80\x0b13///---997799<\x1f%\x1f\"\x1f%)\x1f\"//\x12\x0e\x12\x0f\x12\x10\x12\x11\x12\x12\x12\x13\x12\x14*\x07Y\x12\x15\x12\x16\x12\x17\x12\x18\x12\x19\x12\x1a\x12\x08\x12\x07*\x08Y\x12\x1b\x12\x1c\x12\t\x12\x1d\x12\x1e\x12\x1f*\x06Y\x12 \x12!\x12\"\x12#\x12$\x12%*\x06Y\x12&\x12'\x12(\x12)\x12*\x12+*\x06Y\x12,\x12-\x12.\x12/\x120*\x05Y\x121\x122\x123\x124\x125*\x05Y\x126\x127\x128\x129\x12:*\x05Y\x12;\x12<\x12=\x12>\x12?\x12@\x12A\x12B\x12C\x12D\x12E*\x0bY\x12F\x12G\x12H\x12I\x12J\x12K\x12L\x12M\x12N\x12O\x12P*\x0bY\x12Q\x12R\x12S\x12T\x12U\x12V\x12W\x12X\x12Y\x12Z*\nY\x12[\x12\\\x12]\x12^\x12_\x12`\x12a\x12b\x12c\x12d*\nY\x12e\x12f\x12g\x12h\x12i\x12j\x12k\x12l\x12m\x12n\x12o*\x0bY\x12p\x12q\x12r\x12s\x12t\x12u\x12v\x12w\x12x\x12y\x12z*\x0bY\x12{\x12|\x12}\x12~\x12\x7f\x12\x81\x00\x12\x81\x01\x12\x81\x02\x12\x81\x03\x12\x81\x04*\nY\x12\x81\x05\x12\x81\x06\x12\x81\x07\x12\x81\x08\x12\x81\t\x12\x81\n\x12\x81\x0b\x12\x81\x0c\x12\x81\r\x12\x81\x0e\x12\x81\x0f*\x0bY\x12\x81\x10\x12\x81\x11\x12\x81\x12\x12\x81\x13\x12\x81\x14\x12\x81\x15\x12\x81\x16\x12\x81\x17\x12\x81\x18\x12\x81\x19*\nY\x12\x81\x1a\x12\x81\x1b\x12\x81\x1c\x12\x81\x1d\x12\x81\x1e\x12\x81\x1f\x12\x81 \x12\x81!\x12\x81\"\x12\x81#\x12\x81$*\x0bY\x12\x81%\x12\x81&*\x02Y\x12\x81'\x12\x81(\x12\x81)\x12\x81*\x12\x81+\x12\x81,\x12\x81-\x12\x81.\x12\x81/\x12\x810*\nY\x12\x811\x12\x812\x12\x813\x12\x814*\x04Y\x12\x815\x12\x816\x12\x817\x12\x818*\x04Y\x12\x819\x12\x81:\x12\x81;\x12\x81<*\x04YQc\x87p\x08@\x05\x80###############################\x00\xc0#\x01\xc0#\x02\xc0#\x03\xc0#\x04\xc0#\x05\xc0#\x06\xc0#\x07\xc0#\x08\xc0#\t\xc0#\n\xc0#\x0b\xc0#\x0c\xc0#\r\xc0#\x0e\xc0#\x0f\xc0#\x10\xc0#\x11\xc0#\x12\xc0#\x13\xc0#\x14\xc0#\x15\xc0#\x16\xc0#\x17\xc0#\x18\xc0#\x19\xc0#\x1a\xc0#\x1b\xc0#\x1c\xc0#\x1d\xc0Qc" +file_data = b"M\x06\x00\x1e\x81=\x1e\x0etest.py\x00\x0f\x04A0\x00\x04A1\x00\x04f0\x00\x04f1\x00\x0cresult\x00/-5\x04a0\x00\x04a1\x00\x04a2\x00\x04a3\x00\x13\x15\x17\x19\x1b\x1d\x1f!#%')+1379;=?ACEGIKMOQSUWY[]_acegikmoqsuwy{}\x7f\x81\x01\x81\x03\x81\x05\x81\x07\x81\t\x81\x0b\x81\r\x81\x0f\x81\x11\x81\x13\x81\x15\x81\x17\x81\x19\x81\x1b\x81\x1d\x81\x1f\x81!\x81#\x81%\x81'\x81)\x81+\x81-\x81/\x811\x813\x815\x817\x819\x81;\x81=\x81?\x81A\x81C\x81E\x81G\x81I\x81K\x81M\x81O\x81Q\x81S\x81U\x81W\x81Y\x81[\x81]\x81_\x81a\x81c\x81e\x81g\x81i\x81k\x81m\x81o\x81q\x81s\x81u\x81w\x81y\x81{\x81}\x81\x7f\x82\x01\x82\x03\x82\x05\x82\x07\x82\t\x82\x0b\x82\r\x82\x0f\x82\x11\x82\x13\x82\x15\x82\x17\x82\x19\x82\x1b\x82\x1d\x82\x1f\x82!\x82#\x82%\x82'\x82)\x82+\x82-\x82/\x821\x823\x825\x827\x829\x82;\x82=\x82?\x82A\x82E\x82G\x82I\x82K\nname0\x00\nname1\x00\nname2\x00\nname3\x00\nname4\x00\nname5\x00\nname6\x00\nname7\x00\nname8\x00\nname9\x00$quite_a_long_name0\x00$quite_a_long_name1\x00$quite_a_long_name2\x00$quite_a_long_name3\x00$quite_a_long_name4\x00$quite_a_long_name5\x00$quite_a_long_name6\x00$quite_a_long_name7\x00$quite_a_long_name8\x00$quite_a_long_name9\x00&quite_a_long_name10\x00&quite_a_long_name11\x00\x05\x1ethis will be a string object 0\x00\x05\x1ethis will be a string object 1\x00\x05\x1ethis will be a string object 2\x00\x05\x1ethis will be a string object 3\x00\x05\x1ethis will be a string object 4\x00\x05\x1ethis will be a string object 5\x00\x05\x1ethis will be a string object 6\x00\x05\x1ethis will be a string object 7\x00\x05\x1ethis will be a string object 8\x00\x05\x1ethis will be a string object 9\x00\x06\x1dthis will be a bytes object 0\x00\x06\x1dthis will be a bytes object 1\x00\x06\x1dthis will be a bytes object 2\x00\x06\x1dthis will be a bytes object 3\x00\x06\x1dthis will be a bytes object 4\x00\x06\x1dthis will be a bytes object 5\x00\x06\x1dthis will be a bytes object 6\x00\x06\x1dthis will be a bytes object 7\x00\x06\x1dthis will be a bytes object 8\x00\x06\x1dthis will be a bytes object 9\x00\n\x07\x05\rconst tuple 0\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 1\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 2\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 3\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 4\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 5\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 6\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 7\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 8\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\n\x07\x05\rconst tuple 9\x00\x01\x02\x03\x07\x011\x07\x012\x07\x013\x82d\x10\x12\x01i@i@\x84\x18\x84\x1fT2\x00\x10\x024\x02\x16\x02T2\x01\x10\x034\x02\x16\x032\x02\x16\x042\x03\x16\x05\"\x80{\x16\x06Qc\x04\x82\x0c\x00\n\x02($$$\x11\x07\x16\x08\x10\x02\x16\t2\x00\x16\n2\x01\x16\x0b2\x02\x16\x0c2\x03\x16\rQc\x04@\t\x08\n\x81\x0b Qc@\t\x08\x0b\x81\x0b@Qc@\t\x08\x0c\x81\x0b`QcH\t\n\r\x81\x0b` Qc\x82\x14\x00\x0c\x03h`$$$\x11\x07\x16\x08\x10\x03\x16\t2\x00\x16\n2\x01\x16\x0b2\x02\x16\x0c2\x03\x16\rQc\x04H\t\n\n\x81\x0b``QcH\t\n\x0b\x81\x0b\x80\x07QcH\t\n\x0c\x81\x0b\x80\x08QcH\t\n\r\x81\x0b\x80\tQc\xa08P:\x04\x80\x0b13///---997799<\x1f%\x1f\"\x1f%)\x1f\"//\x12\x0e\x12\x0f\x12\x10\x12\x11\x12\x12\x12\x13\x12\x14*\x07Y\x12\x15\x12\x16\x12\x17\x12\x18\x12\x19\x12\x1a\x12\x08\x12\x07*\x08Y\x12\x1b\x12\x1c\x12\t\x12\x1d\x12\x1e\x12\x1f*\x06Y\x12 \x12!\x12\"\x12#\x12$\x12%*\x06Y\x12&\x12'\x12(\x12)\x12*\x12+*\x06Y\x12,\x12-\x12.\x12/\x120*\x05Y\x121\x122\x123\x124\x125*\x05Y\x126\x127\x128\x129\x12:*\x05Y\x12;\x12<\x12=\x12>\x12?\x12@\x12A\x12B\x12C\x12D\x12E*\x0bY\x12F\x12G\x12H\x12I\x12J\x12K\x12L\x12M\x12N\x12O\x12P*\x0bY\x12Q\x12R\x12S\x12T\x12U\x12V\x12W\x12X\x12Y\x12Z*\nY\x12[\x12\\\x12]\x12^\x12_\x12`\x12a\x12b\x12c\x12d*\nY\x12e\x12f\x12g\x12h\x12i\x12j\x12k\x12l\x12m\x12n\x12o*\x0bY\x12p\x12q\x12r\x12s\x12t\x12u\x12v\x12w\x12x\x12y\x12z*\x0bY\x12{\x12|\x12}\x12~\x12\x7f\x12\x81\x00\x12\x81\x01\x12\x81\x02\x12\x81\x03\x12\x81\x04*\nY\x12\x81\x05\x12\x81\x06\x12\x81\x07\x12\x81\x08\x12\x81\t\x12\x81\n\x12\x81\x0b\x12\x81\x0c\x12\x81\r\x12\x81\x0e\x12\x81\x0f*\x0bY\x12\x81\x10\x12\x81\x11\x12\x81\x12\x12\x81\x13\x12\x81\x14\x12\x81\x15\x12\x81\x16\x12\x81\x17\x12\x81\x18\x12\x81\x19*\nY\x12\x81\x1a\x12\x81\x1b\x12\x81\x1c\x12\x81\x1d\x12\x81\x1e\x12\x81\x1f\x12\x81 \x12\x81!\x12\x81\"\x12\x81#\x12\x81$*\x0bY\x12\x81%\x12\x81&*\x02Y\x12\x81'\x12\x81(\x12\x81)\x12\x81*\x12\x81+\x12\x81,\x12\x81-\x12\x81.\x12\x81/\x12\x810*\nY\x12\x811\x12\x812\x12\x813\x12\x814*\x04Y\x12\x815\x12\x816\x12\x817\x12\x818*\x04Y\x12\x819\x12\x81:\x12\x81;\x12\x81<*\x04YQc\x87p\x08@\x05\x80###############################\x00\xc0#\x01\xc0#\x02\xc0#\x03\xc0#\x04\xc0#\x05\xc0#\x06\xc0#\x07\xc0#\x08\xc0#\t\xc0#\n\xc0#\x0b\xc0#\x0c\xc0#\r\xc0#\x0e\xc0#\x0f\xc0#\x10\xc0#\x11\xc0#\x12\xc0#\x13\xc0#\x14\xc0#\x15\xc0#\x16\xc0#\x17\xc0#\x18\xc0#\x19\xc0#\x1a\xc0#\x1b\xc0#\x1c\xc0#\x1d\xc0Qc" -class File(io.IOBase): +class File(IOBase): def __init__(self): self.off = 0 def ioctl(self, request, arg): - return 0 + if request == 4: # MP_STREAM_CLOSE + return 0 + return -1 def readinto(self, buf): buf[:] = memoryview(file_data)[self.off : self.off + len(buf)] diff --git a/tests/ports/alif_hardware/flash_test.py b/tests/ports/alif_hardware/flash_test.py new file mode 100644 index 00000000000..5be3fc2d02f --- /dev/null +++ b/tests/ports/alif_hardware/flash_test.py @@ -0,0 +1,149 @@ +# MIT license; Copyright (c) 2024 OpenMV LLC. +# +# Note: this test completely erases the filesystem! +# It is intended to be run manually. + +import alif +import hashlib +import os +import sys +import time +import vfs + +hash_algo = "sha256" + + +def flash_make_filesystem(): + try: + vfs.umount("/flash") + except: + pass + bdev = alif.Flash() + vfs.VfsFat.mkfs(bdev) + vfs.mount(vfs.VfsFat(bdev), "/flash") + sys.path.append("/flash") + sys.path.append("/flash/lib") + os.chdir("/flash") + + +def flash_block_test(): + try: + vfs.umount("/flash") + except: + pass + dev = alif.Flash() + + data512 = os.urandom(512) + buf512 = bytearray(512) + block_numbers = tuple(range(32)) + tuple(range(250, 266)) + + print("Block read/write integrity: ", end="") + ok = True + for block_n in block_numbers: + dev.writeblocks(block_n, data512) + dev.readblocks(block_n, buf512) + if buf512 != data512: + ok = False + print(ok) + + print("Block read back integrity: ", end="") + ok = True + for block_n in block_numbers: + dev.readblocks(block_n, buf512) + if buf512 != data512: + ok = False + print(ok) + + N = 16 * 1024 + data_big = os.urandom(N) + t0 = time.ticks_us() + dev.writeblocks(0, data_big) + dt = time.ticks_diff(time.ticks_us(), t0) + print(f"Block write speed: {len(data_big) / 1024 / dt * 1_000_000} KiB/sec") + + buf_big = bytearray(N) + t0 = time.ticks_us() + dev.readblocks(0, buf_big) + dt = time.ticks_diff(time.ticks_us(), t0) + print(f"Block read speed: {len(buf_big) / 1024 / dt * 1_000_000} KiB/sec") + + if buf_big != data_big: + raise RuntimeError("big block read-back failed") + + try: + import uctypes + + xip = memoryview(dev) + except: + xip = None + if xip is not None: + t0 = time.ticks_us() + buf_big[:] = xip[: len(buf_big)] + dt = time.ticks_diff(time.ticks_us(), t0) + print(f"XIP read speed: {len(buf_big) / 1024 / dt * 1_000_000} KiB/sec") + + if buf_big != data_big: + raise RuntimeError("XIP read-back failed") + for i in range(len(buf_big)): + if xip[i] != data_big[i]: + raise RuntimeError("XIP byte-wise read-back failed") + + +def flash_write_verify(path, size=1024 * 1024, block_size=1024): + hash_sum = getattr(hashlib, hash_algo)() + block_count = size // block_size + + print("-" * 16) + print(f"Writing file {size=} {block_size=}") + t0 = time.ticks_ms() + total_size = 0 + with open(path, "wb") as file: + for i in range(block_count): + buf = os.urandom(block_size) + # Update digest + hash_sum.update(buf) + total_size += file.write(buf) + if i % (block_count // 16) == 0: + print(f"{i}/{block_count}", end="\r") + dt = time.ticks_diff(time.ticks_ms(), t0) + print(f"Flash write finished: {total_size / 1024 / dt * 1000} KiB/sec") + digest = hash_sum.digest() + + print("Reading file... ", end="") + hash_sum = getattr(hashlib, hash_algo)() + buf = bytearray(block_size) + t0 = time.ticks_ms() + total_size = 0 + with open(path, "rb") as file: + for i in range(block_count): + total_size += file.readinto(buf) + dt = time.ticks_diff(time.ticks_ms(), t0) + print(f"finished: {total_size / 1024 / dt * 1000} KiB/sec") + + print("Verifying file... ", end="") + hash_sum = getattr(hashlib, hash_algo)() + t0 = time.ticks_ms() + total_size = 0 + with open(path, "rb") as file: + for i in range(block_count): + buf = file.read(block_size) + total_size += len(buf) + # Update digest + hash_sum.update(buf) + dt = time.ticks_diff(time.ticks_ms(), t0) + print(f"finished: {total_size / 1024 / dt * 1000} KiB/sec; ", end="") + + if digest != hash_sum.digest(): + raise RuntimeError(f"{hash_algo} checksum verify failed") + + print(f"{hash_algo} checksum verified") + + +if __name__ == "__main__": + flash_block_test() + flash_make_filesystem() + flash_write_verify("test0.bin", size=64 * 1024, block_size=1024) + flash_write_verify("test1.bin", size=128 * 1024, block_size=1024) + flash_write_verify("test2.bin", size=64 * 1024, block_size=2048) + flash_write_verify("test4.bin", size=256 * 1024, block_size=4096) + flash_write_verify("test4.bin", size=512 * 1024, block_size=16384) diff --git a/tests/ports/cc3200/pin.py b/tests/ports/cc3200/pin.py index 43537bba5ce..d7b0fa976cd 100644 --- a/tests/ports/cc3200/pin.py +++ b/tests/ports/cc3200/pin.py @@ -3,6 +3,7 @@ pull up or pull down connected. GP12 and GP17 must be connected together """ + from machine import Pin import os diff --git a/tests/ports/esp32/check_err_str.py b/tests/ports/esp32/check_err_str.py index 055eecf7476..598db4e1e11 100644 --- a/tests/ports/esp32/check_err_str.py +++ b/tests/ports/esp32/check_err_str.py @@ -1,3 +1,5 @@ +# This tests checks the behaviour of the `check_esp_err`/`check_esp_err_` C function. + try: from esp32 import Partition as p import micropython @@ -23,17 +25,19 @@ # same but with out of memory condition by locking the heap exc = "FAILED TO RAISE" +e = None # preallocate entry in globals dict micropython.heap_lock() try: fun(part) except OSError as e: exc = e micropython.heap_unlock() -print("exc:", exc) # exc empty due to no memory +print("exc:", repr(exc)) # exc empty due to no memory # same again but having an emergency buffer micropython.alloc_emergency_exception_buf(256) exc = "FAILED TO RAISE" +e = None # preallocate entry in globals dict micropython.heap_lock() try: fun(part) diff --git a/tests/ports/esp32/check_err_str.py.exp b/tests/ports/esp32/check_err_str.py.exp index d4162d1be00..db45d492551 100644 --- a/tests/ports/esp32/check_err_str.py.exp +++ b/tests/ports/esp32/check_err_str.py.exp @@ -1,4 +1,4 @@ [Errno 2] ENOENT (-5379, 'ESP_ERR_OTA_VALIDATE_FAILED') -exc: +exc: OSError() -5379 diff --git a/tests/ports/esp32/esp32_idf_heap_info.py b/tests/ports/esp32/esp32_idf_heap_info.py index fdd89161f43..2f45295938d 100644 --- a/tests/ports/esp32/esp32_idf_heap_info.py +++ b/tests/ports/esp32/esp32_idf_heap_info.py @@ -5,6 +5,19 @@ print("SKIP") raise SystemExit +import sys + +# idf_heap_info() is expected to return at least this many +# regions for HEAP_DATA and HEAP_EXEC, respectively. +MIN_DATA = 3 +MIN_EXEC = 3 + +impl = str(sys.implementation) +if "ESP32C2" in impl: + # ESP32-C2 is less fragmented (yay!) and only has two memory regions + MIN_DATA = 2 + MIN_EXEC = 2 + # region tuple is: (size, free, largest free, min free) # we check that each region's size is > 0 and that the free amounts are smaller than the size @@ -22,12 +35,12 @@ def chk_heap(kind, regions): # try getting heap regions regions = esp32.idf_heap_info(esp32.HEAP_DATA) -print("HEAP_DATA >2:", len(regions) > 2) +print("HEAP_DATA >=MIN:", len(regions) >= MIN_DATA) chk_heap("HEAP_DATA", regions) # try getting code regions regions = esp32.idf_heap_info(esp32.HEAP_EXEC) -print("HEAP_EXEC >2:", len(regions) > 2) +print("HEAP_EXEC >=MIN:", len(regions) >= MIN_EXEC) chk_heap("HEAP_EXEC", regions) # try invalid param diff --git a/tests/ports/esp32/esp32_idf_heap_info.py.exp b/tests/ports/esp32/esp32_idf_heap_info.py.exp index 2b63bf3259f..7fc50fc1624 100644 --- a/tests/ports/esp32/esp32_idf_heap_info.py.exp +++ b/tests/ports/esp32/esp32_idf_heap_info.py.exp @@ -1,5 +1,5 @@ -HEAP_DATA >2: True +HEAP_DATA >=MIN: True HEAP_DATA [True, True, True, True] -HEAP_EXEC >2: True +HEAP_EXEC >=MIN: True HEAP_EXEC [True, True, True, True] [] diff --git a/tests/ports/esp32/esp32_nvs.py b/tests/ports/esp32/esp32_nvs.py index fd8b152ca71..e6c308cde2f 100644 --- a/tests/ports/esp32/esp32_nvs.py +++ b/tests/ports/esp32/esp32_nvs.py @@ -4,7 +4,7 @@ nvs = NVS("mp-test") -# test setting and gettin an integer kv +# test setting and getting an integer kv nvs.set_i32("key1", 1234) print(nvs.get_i32("key1")) nvs.set_i32("key2", -503) diff --git a/tests/ports/esp32/resolve_on_connect.py b/tests/ports/esp32/resolve_on_connect.py deleted file mode 100644 index e604ce9ca09..00000000000 --- a/tests/ports/esp32/resolve_on_connect.py +++ /dev/null @@ -1,56 +0,0 @@ -# Test that the esp32's socket module performs DNS resolutions on bind and connect -import sys - -if sys.implementation.name == "micropython" and sys.platform != "esp32": - print("SKIP") - raise SystemExit - -import socket, sys - - -def test_bind_resolves_0_0_0_0(): - s = socket.socket() - try: - s.bind(("0.0.0.0", 31245)) - print("bind actually bound") - s.close() - except Exception as e: - print("bind raised", e) - - -def test_bind_resolves_localhost(): - s = socket.socket() - try: - s.bind(("localhost", 31245)) - print("bind actually bound") - s.close() - except Exception as e: - print("bind raised", e) - - -def test_connect_resolves(): - s = socket.socket() - try: - s.connect(("micropython.org", 80)) - print("connect actually connected") - s.close() - except Exception as e: - print("connect raised", e) - - -def test_connect_non_existent(): - s = socket.socket() - try: - s.connect(("nonexistent.example.com", 80)) - print("connect actually connected") - s.close() - except OSError as e: - print("connect raised OSError") - except Exception as e: - print("connect raised", e) - - -test_funs = [n for n in dir() if n.startswith("test_")] -for f in sorted(test_funs): - print("--", f, end=": ") - eval(f + "()") diff --git a/tests/ports/qemu/asm_test.py b/tests/ports/qemu/asm_test.py index 26c7efe4273..ab5ce6905fd 100644 --- a/tests/ports/qemu/asm_test.py +++ b/tests/ports/qemu/asm_test.py @@ -1,4 +1,10 @@ -import frozen_asm +try: + import frozen_asm_thumb as frozen_asm +except ImportError: + try: + import frozen_asm_rv32 as frozen_asm + except ImportError: + raise ImportError print(frozen_asm.asm_add(1, 2)) print(frozen_asm.asm_add1(3)) diff --git a/tests/ports/qemu/mpy_arch_flags_test.py b/tests/ports/qemu/mpy_arch_flags_test.py new file mode 100644 index 00000000000..c07d92d533d --- /dev/null +++ b/tests/ports/qemu/mpy_arch_flags_test.py @@ -0,0 +1,128 @@ +try: + import sys, io, vfs + + sys.implementation._mpy + io.IOBase +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit + +sys_implementation_mpy = sys.implementation._mpy +arch = (sys_implementation_mpy >> 10) & 0x0F + +# RV32-only for now. +if arch != 11: + print("SKIP") + raise SystemExit + + +class UserFile(io.IOBase): + def __init__(self, data): + self.data = memoryview(data) + self.pos = 0 + + def readinto(self, buf): + n = min(len(buf), len(self.data) - self.pos) + buf[:n] = self.data[self.pos : self.pos + n] + self.pos += n + return n + + def ioctl(self, req, arg): + if req == 4: # MP_STREAM_CLOSE + return 0 + return -1 + + +class UserFS: + def __init__(self, files): + self.files = files + + def mount(self, readonly, mksfs): + pass + + def umount(self): + pass + + def stat(self, path): + if path in self.files: + return (32768, 0, 0, 0, 0, 0, 0, 0, 0, 0) + raise OSError + + def open(self, path, mode): + return UserFile(self.files[path]) + + +def mp_encode_uint(val): + encoded = [val & 0x7F] + val >>= 7 + while val != 0: + encoded.insert(0, 0x80 | (val & 0x7F)) + val >>= 7 + return bytes(encoded) + + +def add_flags(module, flags): + output = bytearray() + output.extend(module[:4]) + output[2] |= 0x40 + output.extend(mp_encode_uint(flags)) + output.extend(module[4:]) + return output + + +arch_flags = sys_implementation_mpy >> 16 +can_test_partial_flags = arch_flags not in [1 << i for i in range(16)] + +# To recreate this string run: +# echo "hello=1" | mpy-cross -s module.py - | python -c 'import sys; print(sys.stdin.buffer.read())' +MPY_FILE_BASE = b"M\x06\x00\x1f\x03\x00\x12module.py\x00\x0f\nhello\x00@\x00\x02\x01\x81\x16\x02Qc" + +user_files = { + "/matching_flags.mpy": add_flags(MPY_FILE_BASE, arch_flags), + "/invalid_flags.mpy": add_flags(MPY_FILE_BASE, (~arch_flags & 0xFFFF)), + "/explicit_no_flags.mpy": add_flags(MPY_FILE_BASE, 0), +} +if can_test_partial_flags: + user_files["/partial_flags_more.mpy"] = add_flags( + MPY_FILE_BASE, (arch_flags | (arch_flags << 1)) & 0xFFFF + ) + user_files["/partial_flags_less.mpy"] = add_flags( + MPY_FILE_BASE, (arch_flags >> 1) & arch_flags + ) + +vfs.mount(UserFS(user_files), "/userfs") +sys.path.append("/userfs") + +import matching_flags + +if not hasattr(matching_flags, "hello"): + print("Improper matching_flags import") + +import explicit_no_flags + +if not hasattr(explicit_no_flags, "hello"): + print("Improper explicit_no_flags import") + +try: + import invalid_flags + + print("Unexpected invalid_flags import") +except ValueError as e: + if str(e) != "incompatible .mpy file": + print("Unexpected invalid_flags import error:", str(e)) + +if can_test_partial_flags: + import partial_flags_less + + if not hasattr(partial_flags_less, "hello"): + print("Improper partial_flags_less import") + + try: + import partial_flags_more + + print("Unexpected partial_flags_more import") + except ValueError as e: + if str(e) != "incompatible .mpy file": + print("Unexpected partial_flags_more import error:", str(e)) + +print("OK") diff --git a/tests/ports/qemu/mpy_arch_flags_test.py.exp b/tests/ports/qemu/mpy_arch_flags_test.py.exp new file mode 100644 index 00000000000..d86bac9de59 --- /dev/null +++ b/tests/ports/qemu/mpy_arch_flags_test.py.exp @@ -0,0 +1 @@ +OK diff --git a/tests/ports/qemu/romfs_test.py b/tests/ports/qemu/romfs_test.py new file mode 100644 index 00000000000..dce847d844f --- /dev/null +++ b/tests/ports/qemu/romfs_test.py @@ -0,0 +1,55 @@ +try: + import binascii, os + + binascii.crc32 + os.listdir +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit + +TEST_FILES = [ + "0x30d83fe5.bin", + "0x37bef0eb.bin", + "0x442f3b5f.bin", + "0x648793fb.bin", + "0x913837b6.bin", + "0xdb14aac7.bin", +] + + +def is_test_partition(): + # Make sure a ROMFS partition exists first and that it is the one used for + # CI tests, otherwise skip the test. + + try: + romfs_files = sorted(os.listdir("/rom")) + except OSError as e: + if "ENODEV" in str(e): + return False, None + raise OSError(e) + + # os.path.exists isn't available so we have to be a bit more creative. + if "romfs_sentinel.txt" not in romfs_files: + return False, None + + try: + with open("/rom/romfs_sentinel.txt", "rt") as s: + data = s.read(128) + return data.startswith("*MPY-ROMFS-TEST-PARTITION*\n"), romfs_files + except OSError as e: + if "ENOENT" in str(e): + return False, None + raise OSError(e) + + +valid_partition, romfs_files = is_test_partition() +if not valid_partition: + print("SKIP") + raise SystemExit + +# The last entry is the CI romfs partition tag. + +print(TEST_FILES == romfs_files[:-1]) +for f in TEST_FILES: + with open("/rom/" + f, "rb") as h: + print(hex(binascii.crc32(h.read())) == f[:-4]) diff --git a/tests/ports/qemu/romfs_test.py.exp b/tests/ports/qemu/romfs_test.py.exp new file mode 100644 index 00000000000..672e08f95cc --- /dev/null +++ b/tests/ports/qemu/romfs_test.py.exp @@ -0,0 +1,7 @@ +True +True +True +True +True +True +True diff --git a/tests/ports/rp2/rp2_dma.py b/tests/ports/rp2/rp2_dma.py index 62ec1dcf24d..436e5ee48ec 100644 --- a/tests/ports/rp2/rp2_dma.py +++ b/tests/ports/rp2/rp2_dma.py @@ -4,103 +4,149 @@ import time import machine import rp2 +import unittest is_rp2350 = "RP2350" in sys.implementation._machine -src = bytes(i & 0xFF for i in range(16 * 1024)) - -print("# test basic usage") - -dma = rp2.DMA() - -# Test printing. -print(dma) - -# Test pack_ctrl/unpack_ctrl. -ctrl_dict = rp2.DMA.unpack_ctrl(dma.pack_ctrl()) -if is_rp2350: - for entry in ("inc_read_rev", "inc_write_rev"): - assert entry in ctrl_dict - del ctrl_dict[entry] -for key, value in sorted(ctrl_dict.items()): - print(key, value) - -# Test register access. -dma.read = 0 -dma.write = 0 -dma.count = 0 -dma.ctrl = dma.pack_ctrl() -print(dma.read, dma.write, dma.count, dma.ctrl & 0x01F, dma.channel, dma.registers) -dma.close() - -# Test closing when already closed. -dma.close() - -# Test using when closed. -try: - dma.active() - assert False -except ValueError: - print("ValueError") - -# Test simple memory copy. -print("# test memory copy") -dest = bytearray(1024) -dma = rp2.DMA() -dma.config(read=src, write=dest, count=len(dest) // 4, ctrl=dma.pack_ctrl(), trigger=False) -print(not any(dest)) -dma.active(True) -while dma.active(): - pass -print(dest[:8], dest[-8:]) -dma.close() - - -# Test time taken for a large memory copy. -def run_and_time_dma(dma): - ticks_us = time.ticks_us - irq_state = machine.disable_irq() - t0 = ticks_us() - dma.active(True) - while dma.active(): - pass - t1 = ticks_us() - machine.enable_irq(irq_state) - return time.ticks_diff(t1, t0) - - -print("# test timing") -dest = bytearray(16 * 1024) -dma = rp2.DMA() -dma.read = src -dma.write = dest -dma.count = len(dest) // 4 -dma.ctrl = dma.pack_ctrl() -dt = run_and_time_dma(dma) -expected_dt_range = range(40, 70) if is_rp2350 else range(70, 125) -print(dt in expected_dt_range) -print(dest[:8], dest[-8:]) -dma.close() - -# Test using .config(trigger=True). -print("# test immediate trigger") -dest = bytearray(1024) -dma = rp2.DMA() -dma.config(read=src, write=dest, count=len(dest) // 4, ctrl=dma.pack_ctrl(), trigger=True) -while dma.active(): - pass -print(dest[:8], dest[-8:]) -dma.close() - -# Test the DMA.irq() method. -print("# test irq") -dest = bytearray(1024) -dma = rp2.DMA() -dma.irq(lambda dma: print("irq fired", dma.irq().flags())) -dma.config( - read=src, write=dest, count=len(dest) // 4, ctrl=dma.pack_ctrl(irq_quiet=0), trigger=True -) -while dma.active(): - pass -print(dest[:8], dest[-8:]) -dma.close() +SRC = bytes(i & 0xFF for i in range(16 * 1024)) + + +class Test(unittest.TestCase): + def setUp(self): + self.dma = rp2.DMA() + + def tearDown(self): + self.dma.close() + + def test_printing(self): + dma = self.dma + self.assertEqual(str(dma), "DMA({})".format(dma.channel)) + + def test_pack_unpack_ctrl(self): + dma = self.dma + ctrl_dict = rp2.DMA.unpack_ctrl(dma.pack_ctrl()) + if is_rp2350: + self.assertEqual(len(ctrl_dict), 18) + self.assertTrue("inc_read_rev" in ctrl_dict) + self.assertTrue("inc_write_rev" in ctrl_dict) + else: + self.assertEqual(len(ctrl_dict), 16) + self.assertEqual(ctrl_dict["ahb_err"], 0) + self.assertEqual(ctrl_dict["bswap"], 0) + self.assertEqual(ctrl_dict["busy"], 0) + self.assertEqual(ctrl_dict["chain_to"], dma.channel) + self.assertEqual(ctrl_dict["enable"], 1) + self.assertEqual(ctrl_dict["high_pri"], 0) + self.assertEqual(ctrl_dict["inc_read"], 1) + self.assertEqual(ctrl_dict["inc_write"], 1) + self.assertEqual(ctrl_dict["irq_quiet"], 1) + self.assertEqual(ctrl_dict["read_err"], 0) + self.assertEqual(ctrl_dict["ring_sel"], 0) + self.assertEqual(ctrl_dict["ring_size"], 0) + self.assertEqual(ctrl_dict["size"], 2) + self.assertEqual(ctrl_dict["sniff_en"], 0) + self.assertEqual(ctrl_dict["treq_sel"], 63) + self.assertEqual(ctrl_dict["write_err"], 0) + + def test_register_access(self): + dma = self.dma + dma.read = 0 + dma.write = 0 + dma.count = 0 + dma.ctrl = dma.pack_ctrl() + self.assertEqual(dma.read, 0) + self.assertEqual(dma.write, 0) + self.assertEqual(dma.count, 0) + self.assertEqual(dma.ctrl & 0x01F, 25) + self.assertIn(dma.channel, range(16)) + self.assertIsInstance(dma.registers, memoryview) + + def test_close(self): + dma = self.dma + dma.close() + + # Test closing when already closed. + dma.close() + + # Test using when closed. + with self.assertRaises(ValueError): + dma.active() + + def test_simple_memory_copy(self): + dma = self.dma + dest = bytearray(1024) + dma.config(read=SRC, write=dest, count=len(dest) // 4, ctrl=dma.pack_ctrl(), trigger=False) + self.assertFalse(any(dest)) + dma.active(True) + while dma.active(): + pass + self.assertEqual(dest[:8], SRC[:8]) + self.assertEqual(dest[-8:], SRC[-8:]) + + def test_time_taken_for_large_memory_copy(self): + def run_and_time_dma(dma): + ticks_us = time.ticks_us + active = dma.active + irq_state = machine.disable_irq() + t0 = ticks_us() + active(True) + while active(): + pass + t1 = ticks_us() + machine.enable_irq(irq_state) + return time.ticks_diff(t1, t0) + + num_average = 10 + dt_sum = 0 + for _ in range(num_average): + dma = self.dma + dest = bytearray(16 * 1024) + dma.read = SRC + dma.write = dest + dma.count = len(dest) // 4 + dma.ctrl = dma.pack_ctrl() + dt_sum += run_and_time_dma(dma) + self.assertEqual(dest[:8], SRC[:8]) + self.assertEqual(dest[-8:], SRC[-8:]) + self.tearDown() + self.setUp() + dt = dt_sum // num_average + self.assertIn(dt, range(30, 80)) + + def test_config_trigger(self): + # Test using .config(trigger=True) to start DMA immediately. + dma = self.dma + dest = bytearray(1024) + dma.config(read=SRC, write=dest, count=len(dest) // 4, ctrl=dma.pack_ctrl(), trigger=True) + while dma.active(): + pass + self.assertEqual(dest[:8], SRC[:8]) + self.assertEqual(dest[-8:], SRC[-8:]) + + def test_irq(self): + def callback(dma): + nonlocal irq_flags + print("irq fired") + irq_flags = dma.irq().flags() + + dma = self.dma + irq_flags = None + dest = bytearray(1024) + dma.irq(callback) + dma.config( + read=SRC, + write=dest, + count=len(dest) // 4, + ctrl=dma.pack_ctrl(irq_quiet=0), + trigger=True, + ) + while dma.active(): + pass + time.sleep_ms(1) # when running as native code, give the scheduler a chance to run + self.assertEqual(irq_flags, 1) + self.assertEqual(dest[:8], SRC[:8]) + self.assertEqual(dest[-8:], SRC[-8:]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/ports/rp2/rp2_dma.py.exp b/tests/ports/rp2/rp2_dma.py.exp deleted file mode 100644 index 6fad5429b29..00000000000 --- a/tests/ports/rp2/rp2_dma.py.exp +++ /dev/null @@ -1,31 +0,0 @@ -# test basic usage -DMA(0) -ahb_err 0 -bswap 0 -busy 0 -chain_to 0 -enable 1 -high_pri 0 -inc_read 1 -inc_write 1 -irq_quiet 1 -read_err 0 -ring_sel 0 -ring_size 0 -size 2 -sniff_en 0 -treq_sel 63 -write_err 0 -0 0 0 25 0 -ValueError -# test memory copy -True -bytearray(b'\x00\x01\x02\x03\x04\x05\x06\x07') bytearray(b'\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff') -# test timing -True -bytearray(b'\x00\x01\x02\x03\x04\x05\x06\x07') bytearray(b'\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff') -# test immediate trigger -bytearray(b'\x00\x01\x02\x03\x04\x05\x06\x07') bytearray(b'\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff') -# test irq -irq fired 1 -bytearray(b'\x00\x01\x02\x03\x04\x05\x06\x07') bytearray(b'\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff') diff --git a/tests/ports/rp2/rp2_lightsleep.py b/tests/ports/rp2/rp2_lightsleep.py index 5ce5696e0e1..63f31940ad0 100644 --- a/tests/ports/rp2/rp2_lightsleep.py +++ b/tests/ports/rp2/rp2_lightsleep.py @@ -16,12 +16,6 @@ except ImportError: print("SKIP") raise SystemExit - -# RP2350 currently fails this test, needs further investigation. -if "RP2350" in sys.implementation._machine: - print("SKIP") - raise SystemExit - try: led = Pin(Pin.board.LED, Pin.OUT) except AttributeError: diff --git a/tests/ports/rp2/rp2_lightsleep_regs.py b/tests/ports/rp2/rp2_lightsleep_regs.py new file mode 100644 index 00000000000..4a833d0a9c3 --- /dev/null +++ b/tests/ports/rp2/rp2_lightsleep_regs.py @@ -0,0 +1,58 @@ +# Test that SLEEP_ENx registers are preserved over a call to machine.lightsleep(). + +import sys +from machine import mem32, lightsleep +import unittest + +is_rp2350 = "RP2350" in sys.implementation._machine + +if is_rp2350: + CLOCK_BASE = 0x40010000 + SLEEP_EN0 = CLOCK_BASE + 0xB4 + SLEEP_EN1 = CLOCK_BASE + 0xB8 + TO_DISABLE_EN0 = 1 << 30 # SHA256 + TO_DISABLE_EN1 = 1 << 4 # SRAM0 +else: + CLOCK_BASE = 0x40008000 + SLEEP_EN0 = CLOCK_BASE + 0xA8 + SLEEP_EN1 = CLOCK_BASE + 0xAC + TO_DISABLE_EN0 = 1 << 28 # SRAM0 + TO_DISABLE_EN1 = 1 << 0 # SRAM4 + + +class Test(unittest.TestCase): + def setUp(self): + self.orig_sleep_en0 = mem32[SLEEP_EN0] + self.orig_sleep_en1 = mem32[SLEEP_EN1] + + def tearDown(self): + mem32[SLEEP_EN0] = self.orig_sleep_en0 + mem32[SLEEP_EN1] = self.orig_sleep_en1 + + def test_sleep_en_regs(self): + print() + + # Disable some bits so the registers aren't just 0xffff. + mem32[SLEEP_EN0] &= ~TO_DISABLE_EN0 + mem32[SLEEP_EN1] &= ~TO_DISABLE_EN1 + + # Get the registers before the lightsleep. + sleep_en0_before = mem32[SLEEP_EN0] & 0xFFFFFFFF + sleep_en1_before = mem32[SLEEP_EN1] & 0xFFFFFFFF + print(hex(sleep_en0_before), hex(sleep_en1_before)) + + # Do a lightsleep. + lightsleep(100) + + # Get the registers after a lightsleep. + sleep_en0_after = mem32[SLEEP_EN0] & 0xFFFFFFFF + sleep_en1_after = mem32[SLEEP_EN1] & 0xFFFFFFFF + print(hex(sleep_en0_after), hex(sleep_en1_after)) + + # Check the registers have not changed. + self.assertEqual(sleep_en0_before, sleep_en0_after) + self.assertEqual(sleep_en1_before, sleep_en1_after) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/ports/rp2/rp2_lightsleep_thread.py b/tests/ports/rp2/rp2_lightsleep_thread.py new file mode 100644 index 00000000000..90db6c4a5f8 --- /dev/null +++ b/tests/ports/rp2/rp2_lightsleep_thread.py @@ -0,0 +1,71 @@ +# Verify that a thread running on CPU1 can go to lightsleep +# and wake up in the expected timeframe +import _thread +import time +import unittest +from machine import lightsleep + +N_SLEEPS = 5 +SLEEP_MS = 250 + +IDEAL_RUNTIME = N_SLEEPS * SLEEP_MS +MIN_RUNTIME = 2 * SLEEP_MS +MAX_RUNTIME = (N_SLEEPS + 1) * SLEEP_MS +MAX_DELTA = 20 + + +class LightSleepInThread(unittest.TestCase): + def thread_entry(self, is_thread=True): + for _ in range(N_SLEEPS): + lightsleep(SLEEP_MS) + if is_thread: + self.thread_done = True + + def elapsed_ms(self): + return time.ticks_diff(time.ticks_ms(), self.t0) + + def setUp(self): + self.thread_done = False + self.t0 = time.ticks_ms() + + def test_cpu0_busy(self): + _thread.start_new_thread(self.thread_entry, ()) + # CPU0 is busy-waiting not asleep itself + while not self.thread_done: + self.assertLessEqual(self.elapsed_ms(), MAX_RUNTIME) + self.assertAlmostEqual(self.elapsed_ms(), IDEAL_RUNTIME, delta=MAX_DELTA) + + def test_cpu0_sleeping(self): + _thread.start_new_thread(self.thread_entry, ()) + time.sleep_ms(MAX_RUNTIME) + self.assertTrue(self.thread_done) + self.assertAlmostEqual(self.elapsed_ms(), MAX_RUNTIME, delta=MAX_DELTA) + + def test_cpu0_also_lightsleep(self): + _thread.start_new_thread(self.thread_entry, ()) + time.sleep_ms(50) # account for any delay in starting the thread + self.thread_entry(False) # does the same lightsleep loop, doesn't set the done flag + while not self.thread_done: + time.sleep_ms(10) + # + # Only one thread can actually be in lightsleep at a time to avoid + # races, but otherwise the behaviour when both threads call lightsleep() + # is unspecified. + # + # Currently, the other thread will return immediately if one is already + # in lightsleep doing set up, or waking up. When a thread is actually in the + # sleep section of lightsleep, the CPU clock is stopped and that also stops + # the other thread. It's possible for the total sleep time of this test to + # be less than IDEAL_RUNTIME due to the order in which each thread gets to go + # to sleep first and whether the other thread is paused before or after it + # tries to enter lightsleep itself. + # + # Note this test case is really only here to ensure that the rp2 hasn't + # hung or failed to sleep at all - not to verify any correct behaviour + # when there's a race to call lightsleep(). + self.assertGreaterEqual(self.elapsed_ms(), MIN_RUNTIME - MAX_DELTA) + self.assertLessEqual(self.elapsed_ms(), IDEAL_RUNTIME * 2 + MAX_DELTA) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/ports/rp2/rp2_machine_idle.py b/tests/ports/rp2/rp2_machine_idle.py index 3135110b820..f9c28284782 100644 --- a/tests/ports/rp2/rp2_machine_idle.py +++ b/tests/ports/rp2/rp2_machine_idle.py @@ -1,4 +1,3 @@ -import sys import machine import time @@ -18,11 +17,6 @@ # Verification uses the average idle time, as individual iterations will always # have outliers due to interrupts, scheduler, etc. -# RP2350 currently fails this test because machine.idle() resumes immediately. -if "RP2350" in sys.implementation._machine: - print("SKIP") - raise SystemExit - ITERATIONS = 500 total = 0 diff --git a/tests/ports/rp2/rp2_thread_reset_part1.py b/tests/ports/rp2/rp2_thread_reset_part1.py new file mode 100644 index 00000000000..d43868113f5 --- /dev/null +++ b/tests/ports/rp2/rp2_thread_reset_part1.py @@ -0,0 +1,18 @@ +# This is a regression test for https://github.com/micropython/micropython/issues/16619 +# it runs in two parts by necessity: +# +# - This "part1" creates a non-terminating thread. +# - The test runner issues a soft reset, which will terminate that thread. +# - "part2" is the actual test, which is whether flash access works correctly +# after the thread was terminated by soft reset. + +import _thread + + +def infinite(): + while True: + pass + + +_thread.start_new_thread(infinite, ()) +print("Part 1 complete...") diff --git a/tests/ports/rp2/rp2_thread_reset_part1.py.exp b/tests/ports/rp2/rp2_thread_reset_part1.py.exp new file mode 100644 index 00000000000..48ea7efad97 --- /dev/null +++ b/tests/ports/rp2/rp2_thread_reset_part1.py.exp @@ -0,0 +1 @@ +Part 1 complete... diff --git a/tests/ports/rp2/rp2_thread_reset_part2.py b/tests/ports/rp2/rp2_thread_reset_part2.py new file mode 100644 index 00000000000..15f0eaab8f8 --- /dev/null +++ b/tests/ports/rp2/rp2_thread_reset_part2.py @@ -0,0 +1,12 @@ +# This is part2 of a two-part regression test, see part1 +# for details of what's expected. +import os + +FILENAME = "/rp2_thread_reset_test.txt" + +print("Starting") +with open(FILENAME, "w") as f: + f.write("test") +print("Written") +os.unlink(FILENAME) +print("Removed") diff --git a/tests/ports/rp2/rp2_thread_reset_part2.py.exp b/tests/ports/rp2/rp2_thread_reset_part2.py.exp new file mode 100644 index 00000000000..d7581c7d260 --- /dev/null +++ b/tests/ports/rp2/rp2_thread_reset_part2.py.exp @@ -0,0 +1,3 @@ +Starting +Written +Removed diff --git a/tests/ports/stm32/adc.py b/tests/ports/stm32/adc.py index 875d31d732c..299a5af9c63 100644 --- a/tests/ports/stm32/adc.py +++ b/tests/ports/stm32/adc.py @@ -1,5 +1,10 @@ +import sys from pyb import ADC, Timer +if "STM32WB" in sys.implementation._machine: + print("SKIP") + raise SystemExit + adct = ADC(16) # Temperature 930 -> 20C print(str(adct)[:19]) adcv = ADC(17) # Voltage 1500 -> 3.3V diff --git a/tests/ports/stm32/adcall.py b/tests/ports/stm32/adcall.py index cfe179a97b2..18896c40cb2 100644 --- a/tests/ports/stm32/adcall.py +++ b/tests/ports/stm32/adcall.py @@ -1,5 +1,13 @@ +import sys from pyb import Pin, ADCAll +if "STM32WB" in sys.implementation._machine: + pa0_adc_channel = 5 + skip_temp_test = True # temperature fails on WB55 +else: + pa0_adc_channel = 0 + skip_temp_test = False + pins = [Pin.cpu.A0, Pin.cpu.A1, Pin.cpu.A2, Pin.cpu.A3] # set pins to IN mode, init ADCAll, then check pins are ANALOG @@ -12,7 +20,7 @@ # set pins to IN mode, init ADCAll with mask, then check some pins are ANALOG for p in pins: p.init(p.IN) -adc = ADCAll(12, 0x70003) +adc = ADCAll(12, 0x70000 | 3 << pa0_adc_channel) for p in pins: print(p) @@ -25,7 +33,11 @@ print(type(adc.read_channel(c))) # call special reading functions -print(0 < adc.read_core_temp() < 100) +print(skip_temp_test or 0 < adc.read_core_temp() < 100) print(0 < adc.read_core_vbat() < 4) print(0 < adc.read_core_vref() < 2) print(0 < adc.read_vref() < 4) + +if sys.implementation._build == "NUCLEO_WB55": + # Restore button pin settings. + Pin("SW", Pin.IN, Pin.PULL_UP) diff --git a/tests/ports/stm32/can.py.exp b/tests/ports/stm32/can.py.exp deleted file mode 100644 index bd8f6d60b60..00000000000 --- a/tests/ports/stm32/can.py.exp +++ /dev/null @@ -1,74 +0,0 @@ -ValueError -1 -ValueError 0 -CAN 1 -ValueError 3 -CAN(1) -True -CAN(1, CAN.LOOPBACK, auto_restart=False) -False -True -[0, 0, 0, 0, 0, 0, 0, 0] -True [0, 0, 0, 0, 0, 0, 1, 0] -(123, False, False, 0, b'abcd') -(2047, False, False, 0, b'abcd') -(0, False, False, 0, b'abcd') -passed -[42, False, False, 0, ] 0 bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -[42, False, False, 0, ] 4 bytearray(b'1234\x00\x00\x00\x00\x00\x00') -[42, False, False, 0, ] 8 bytearray(b'01234567\x00\x00') -[42, False, False, 0, ] 3 bytearray(b'abc34567\x00\x00') -b'abc' -b'def' -TypeError -ValueError -TypeError -ValueError -ValueError -==== TEST extframe=True ==== -CAN(1, CAN.LOOPBACK, auto_restart=False) -passed -('0x8', '0x1c', '0xa', True, b'ok') -('0x800', '0x1c00', '0xa00', True, b'ok') -('0x80000', '0x1c0000', '0xa0000', True, b'ok') -('0x8000000', '0x1c000000', '0xa000000', True, b'ok') -==== TEST rx callbacks ==== -cb0 -pending -cb0 -full -cb0a -overflow -cb1 -pending -cb1 -full -cb1a -overflow -(1, False, False, 0, b'11111111') -(2, False, False, 1, b'22222222') -(4, False, False, 3, b'44444444') -(5, False, False, 0, b'55555555') -(6, False, False, 1, b'66666666') -(8, False, False, 3, b'88888888') -cb0a -pending -cb1a -pending -(1, False, False, 0, b'11111111') -(5, False, False, 0, b'55555555') -==== TEST async send ==== -False -(1, False, False, 0, b'abcde') -passed -(2, False, False, 0, b'abcde') -(3, False, False, 0, b'abcde') -(4, False, False, 0, b'abcde') -==== TEST rtr messages ==== -False -(5, False, True, 4, b'') -(6, False, True, 5, b'') -(7, False, True, 6, b'') -False -(32, False, True, 9, b'') -==== TEST errors ==== -OSError(110,) diff --git a/tests/ports/stm32/can2.py b/tests/ports/stm32/can2.py deleted file mode 100644 index 2ce438f1af9..00000000000 --- a/tests/ports/stm32/can2.py +++ /dev/null @@ -1,25 +0,0 @@ -try: - from pyb import CAN - - CAN(2) -except (ImportError, ValueError): - print("SKIP") - raise SystemExit - -# Testing rtr messages -bus2 = CAN(2, CAN.LOOPBACK) -while bus2.any(0): - bus2.recv(0) -bus2.setfilter(0, CAN.LIST32, 0, (1, 2), rtr=(True, True), extframe=True) -bus2.setfilter(1, CAN.LIST32, 0, (3, 4), rtr=(True, False), extframe=True) -bus2.setfilter(2, CAN.MASK32, 0, (16, 16), rtr=(False,), extframe=True) -bus2.setfilter(2, CAN.MASK32, 0, (32, 32), rtr=(True,), extframe=True) - -bus2.send("", 1, rtr=True, extframe=True) -print(bus2.recv(0)) -bus2.send("", 2, rtr=True, extframe=True) -print(bus2.recv(0)) -bus2.send("", 3, rtr=True, extframe=True) -print(bus2.recv(0)) -bus2.send("", 4, rtr=True, extframe=True) -print(bus2.any(0)) diff --git a/tests/ports/stm32/can2.py.exp b/tests/ports/stm32/can2.py.exp deleted file mode 100644 index 3339e5cbeca..00000000000 --- a/tests/ports/stm32/can2.py.exp +++ /dev/null @@ -1,4 +0,0 @@ -(1, True, True, 0, b'') -(2, True, True, 1, b'') -(3, True, True, 2, b'') -False diff --git a/tests/ports/stm32/extint.py b/tests/ports/stm32/extint.py index 5510600020e..d3161f7cc76 100644 --- a/tests/ports/stm32/extint.py +++ b/tests/ports/stm32/extint.py @@ -1,7 +1,8 @@ import pyb # test basic functionality -ext = pyb.ExtInt("X5", pyb.ExtInt.IRQ_RISING, pyb.Pin.PULL_DOWN, lambda l: print("line:", l)) +pin = pyb.Pin.cpu.A4 +ext = pyb.ExtInt(pin, pyb.ExtInt.IRQ_RISING, pyb.Pin.PULL_DOWN, lambda l: print("line:", l)) ext.disable() ext.enable() print(ext.line()) diff --git a/tests/ports/stm32/i2c.py b/tests/ports/stm32/i2c.py index c968843273a..7e7fd250408 100644 --- a/tests/ports/stm32/i2c.py +++ b/tests/ports/stm32/i2c.py @@ -1,5 +1,8 @@ -import pyb -from pyb import I2C +try: + from pyb import I2C +except ImportError: + print("SKIP") + raise SystemExit # test we can correctly create by id for bus in (-1, 0, 1): diff --git a/tests/ports/stm32/i2c_accel.py b/tests/ports/stm32/i2c_accel.py index 8b87d406d06..11ff1392ba4 100644 --- a/tests/ports/stm32/i2c_accel.py +++ b/tests/ports/stm32/i2c_accel.py @@ -1,15 +1,14 @@ # use accelerometer to test i2c bus -import pyb -from pyb import I2C - -if not hasattr(pyb, "Accel"): +try: + from pyb import Accel, I2C +except ImportError: print("SKIP") raise SystemExit accel_addr = 76 -pyb.Accel() # this will init the MMA for us +Accel() # this will init the MMA for us i2c = I2C(1, I2C.CONTROLLER, baudrate=400000) diff --git a/tests/ports/stm32/i2c_error.py b/tests/ports/stm32/i2c_error.py index 1228962f5f4..de6e1ca6fec 100644 --- a/tests/ports/stm32/i2c_error.py +++ b/tests/ports/stm32/i2c_error.py @@ -1,12 +1,13 @@ # test I2C errors, with polling (disabled irqs) and DMA import pyb -from pyb import I2C if not hasattr(pyb, "Accel"): print("SKIP") raise SystemExit +from pyb import I2C + # init accelerometer pyb.Accel() diff --git a/tests/ports/stm32/irq.py b/tests/ports/stm32/irq.py index 04e70a7b792..fd8742d3eaf 100644 --- a/tests/ports/stm32/irq.py +++ b/tests/ports/stm32/irq.py @@ -1,3 +1,4 @@ +import time import pyb @@ -8,7 +9,7 @@ def test_irq(): pyb.enable_irq() # by default should enable IRQ # check that interrupts are enabled by waiting for ticks - pyb.delay(10) + time.sleep_ms(10) # check nested disable/enable i1 = pyb.disable_irq() @@ -18,7 +19,7 @@ def test_irq(): pyb.enable_irq(i1) # check that interrupts are enabled by waiting for ticks - pyb.delay(10) + time.sleep_ms(10) test_irq() diff --git a/tests/ports/stm32/modstm.py b/tests/ports/stm32/modstm.py index f1e147c0529..1459ee2a9e3 100644 --- a/tests/ports/stm32/modstm.py +++ b/tests/ports/stm32/modstm.py @@ -1,13 +1,13 @@ # test stm module import stm -import pyb +import time # test storing a full 32-bit number # turn on then off the A15(=yellow) LED BSRR = 0x18 stm.mem32[stm.GPIOA + BSRR] = 0x00008000 -pyb.delay(100) +time.sleep_ms(100) print(hex(stm.mem32[stm.GPIOA + stm.GPIO_ODR] & 0x00008000)) stm.mem32[stm.GPIOA + BSRR] = 0x80000000 print(hex(stm.mem32[stm.GPIOA + stm.GPIO_ODR] & 0x00008000)) diff --git a/tests/ports/stm32/pin.py b/tests/ports/stm32/pin.py index 3d2bef97e34..cbc78e68abd 100644 --- a/tests/ports/stm32/pin.py +++ b/tests/ports/stm32/pin.py @@ -1,14 +1,20 @@ +import sys from pyb import Pin -p = Pin("X8", Pin.IN) +if "PYB" in sys.implementation._machine: + test_pin = "X8" +else: + test_pin = Pin.cpu.A7 + +p = Pin(test_pin, Pin.IN) print(p) print(p.name()) print(p.pin()) print(p.port()) -p = Pin("X8", Pin.IN, Pin.PULL_UP) -p = Pin("X8", Pin.IN, pull=Pin.PULL_UP) -p = Pin("X8", mode=Pin.IN, pull=Pin.PULL_UP) +p = Pin(test_pin, Pin.IN, Pin.PULL_UP) +p = Pin(test_pin, Pin.IN, pull=Pin.PULL_UP) +p = Pin(test_pin, mode=Pin.IN, pull=Pin.PULL_UP) print(p) print(p.value()) diff --git a/tests/ports/stm32/pyb1.py b/tests/ports/stm32/pyb1.py index e9626ecf4e7..5627946dbc3 100644 --- a/tests/ports/stm32/pyb1.py +++ b/tests/ports/stm32/pyb1.py @@ -2,6 +2,10 @@ import pyb +if not hasattr(pyb, "delay"): + print("SKIP") + raise SystemExit + # test delay pyb.delay(-1) diff --git a/tests/ports/stm32/can.py b/tests/ports/stm32/pyb_can.py similarity index 51% rename from tests/ports/stm32/can.py rename to tests/ports/stm32/pyb_can.py index 020efae0531..8178d91fe74 100644 --- a/tests/ports/stm32/can.py +++ b/tests/ports/stm32/pyb_can.py @@ -7,6 +7,15 @@ from array import array import micropython import pyb +import sys + +# Classic CAN (aka bxCAN) hardware has a different filter API +# and some different behaviours to newer FDCAN hardware +IS_CLASSIC = hasattr(CAN, "MASK16") + +# STM32H7 series has a gold-plated FDCAN peripheral with much deeper TX Queue +# than all other parts +IS_H7 = (not IS_CLASSIC) and "STM32H7" in str(sys.implementation) # test we can correctly create by id (2 handled in can2.py test) for bus in (-1, 0, 1, 3): @@ -25,22 +34,26 @@ can.init(CAN.LOOPBACK, num_filter_banks=14) print(can) -print(can.any(0)) +print("any", can.any(0)) # Test state when freshly created -print(can.state() == can.ERROR_ACTIVE) +print("error_active", can.state() == can.ERROR_ACTIVE) # Test that restart can be called can.restart() # Test info returns a sensible value -print(can.info()) +print("info", can.info()) -# Catch all filter -can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0)) +# Catch all filter (standard IDs) +if IS_CLASSIC: + can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0)) +else: + can.setfilter(0, CAN.MASK, 0, (0, 0), extframe=False) can.send("abcd", 123, timeout=5000) -print(can.any(0), can.info()) +pyb.delay(10) # For FDCAN, needs some time to send +print("any+info", can.any(0), can.info()) print(can.recv(0)) can.send("abcd", -1, timeout=5000) @@ -51,11 +64,16 @@ # Test too long message try: - can.send("abcdefghi", 0x7FF, timeout=5000) + if IS_CLASSIC: + payload = "abcdefghi" # 9 bytes long + else: + # the pyb.CAN API for FDCAN always accepts messages up to 64 bytes and sends as FDCAN + payload = b"x" * 65 + can.send(payload, 0x7FF, timeout=5000) except ValueError: - print("passed") + print("overlong passed") else: - print("failed") + print("overlong failed") # Test that recv can work without allocating memory on the heap @@ -135,7 +153,13 @@ can = CAN(1, CAN.LOOPBACK) # Catch all filter, but only for extframe's -can.setfilter(0, CAN.MASK32, 0, (0, 0), extframe=True) +if IS_CLASSIC: + can.setfilter(0, CAN.MASK32, 0, (0, 0), extframe=True) +else: + # FDCAN manages standard and extframe IDs independently, so need to + # clear the standard ID filter and then set the extended ID filter + can.clearfilter(0, extframe=False) + can.setfilter(0, CAN.MASK, 0, (0, 0), extframe=True) print(can) @@ -144,11 +168,12 @@ except ValueError: print("failed") else: + pyb.delay(10) r = can.recv(0) if r[0] == 0x7FF + 1 and r[4] == b"abcde": - print("passed") + print("extframe passed") else: - print("failed, wrong data received") + print("failed, wrong data received", r) # Test filters for n in [0, 8, 16, 24]: @@ -158,109 +183,40 @@ id_fail = 0b00011010 << n can.clearfilter(0, extframe=True) - can.setfilter(0, pyb.CAN.MASK32, 0, (filter_id, filter_mask), extframe=True) + if IS_CLASSIC: + can.setfilter(0, CAN.MASK32, 0, (filter_id, filter_mask), extframe=True) + else: + can.setfilter(0, CAN.MASK, 0, (filter_id, filter_mask), extframe=True) - can.send("ok", id_ok, timeout=3, extframe=True) + can.send("ok", id_ok, timeout=5, extframe=True) + pyb.delay(10) if can.any(0): msg = can.recv(0) print((hex(filter_id), hex(filter_mask), hex(msg[0]), msg[1], msg[4])) - can.send("fail", id_fail, timeout=3, extframe=True) + can.send("fail", id_fail, timeout=5, extframe=True) + pyb.delay(10) if can.any(0): msg = can.recv(0) print((hex(filter_id), hex(filter_mask), hex(msg[0]), msg[1], msg[4])) del can -# Test RxCallbacks -print("==== TEST rx callbacks ====") - -can = CAN(1, CAN.LOOPBACK) -can.setfilter(0, CAN.LIST16, 0, (1, 2, 3, 4)) -can.setfilter(1, CAN.LIST16, 1, (5, 6, 7, 8)) - - -def cb0(bus, reason): - print("cb0") - if reason == 0: - print("pending") - if reason == 1: - print("full") - if reason == 2: - print("overflow") - - -def cb1(bus, reason): - print("cb1") - if reason == 0: - print("pending") - if reason == 1: - print("full") - if reason == 2: - print("overflow") - - -def cb0a(bus, reason): - print("cb0a") - if reason == 0: - print("pending") - if reason == 1: - print("full") - if reason == 2: - print("overflow") - - -def cb1a(bus, reason): - print("cb1a") - if reason == 0: - print("pending") - if reason == 1: - print("full") - if reason == 2: - print("overflow") - - -can.rxcallback(0, cb0) -can.rxcallback(1, cb1) - -can.send("11111111", 1, timeout=5000) -can.send("22222222", 2, timeout=5000) -can.send("33333333", 3, timeout=5000) -can.rxcallback(0, cb0a) -can.send("44444444", 4, timeout=5000) - -can.send("55555555", 5, timeout=5000) -can.send("66666666", 6, timeout=5000) -can.send("77777777", 7, timeout=5000) -can.rxcallback(1, cb1a) -can.send("88888888", 8, timeout=5000) - -print(can.recv(0)) -print(can.recv(0)) -print(can.recv(0)) -print(can.recv(1)) -print(can.recv(1)) -print(can.recv(1)) - -can.send("11111111", 1, timeout=5000) -can.send("55555555", 5, timeout=5000) - -print(can.recv(0)) -print(can.recv(1)) - -del can - # Testing asynchronous send print("==== TEST async send ====") can = CAN(1, CAN.LOOPBACK) -can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0)) +# Catch all filter (standard IDs) +if IS_CLASSIC: + can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0)) +else: + can.setfilter(0, CAN.MASK, 0, (0, 0), extframe=False) while can.any(0): can.recv(0) can.send("abcde", 1, timeout=0) -print(can.any(0)) +print("any", can.any(0)) while not can.any(0): pass @@ -270,45 +226,82 @@ def cb1a(bus, reason): can.send("abcde", 2, timeout=0) can.send("abcde", 3, timeout=0) can.send("abcde", 4, timeout=0) - can.send("abcde", 5, timeout=0) + if not IS_H7: + can.send("abcde", 5, timeout=0) + else: + # Hack around the STM32H7's deeper transmit queue by pretending this call failed + # (STM32G4 will fail here, using otherwise the same code, so there is still some test coverage.) + print("send fail ok") except OSError as e: - if str(e) == "16": - print("passed") + # When send() fails Classic CAN raises OSError(MP_EBUSY) (16), FDCAN raises OSError(MP_ETIMEDOUT) (110) + if e.errno == (16 if IS_CLASSIC else 110): + print("send fail ok") else: - print("failed") + print("send fail not ok", e) pyb.delay(500) while can.any(0): print(can.recv(0)) +del can + # Testing rtr messages print("==== TEST rtr messages ====") bus1 = CAN(1, CAN.LOOPBACK) while bus1.any(0): bus1.recv(0) -bus1.setfilter(0, CAN.LIST16, 0, (1, 2, 3, 4)) -bus1.setfilter(1, CAN.LIST16, 0, (5, 6, 7, 8), rtr=(True, True, True, True)) -bus1.setfilter(2, CAN.MASK16, 0, (64, 64, 32, 32), rtr=(False, True)) + +if IS_CLASSIC: + # pyb.CAN Classic API allows distinguishing between RTR in the filter + bus1.setfilter(0, CAN.LIST16, 0, (1, 2, 3, 4)) + bus1.setfilter(1, CAN.LIST16, 0, (5, 6, 7, 8), rtr=(True, True, True, True)) + bus1.setfilter(2, CAN.MASK16, 0, (64, 64, 32, 32), rtr=(False, True)) +else: + # pyb.CAN FDCAN API does not allow distinguishing RTR in filter args, so + # instead we'll only filter the message IDs where Classic CAN equivalent is + # setting the RTR flag (meaning we're verifying RTR is received correctly, but + # not verifying the missing filter behaviour.) + bus1.setfilter(0, CAN.RANGE, 0, (5, 8)) + bus1.setfilter(1, CAN.MASK, 0, (32, 32)) + + +def print_rtr(msg): + if msg: + # Skip printing msg[3] as this is the filter match index, and the value + # is different between Classic and FDCAN implementations + print(msg[0], msg[1], msg[2], msg[4]) + else: + print(msg) + bus1.send("", 1, rtr=True) -print(bus1.any(0)) +print("any", bus1.any(0)) bus1.send("", 5, rtr=True) -print(bus1.recv(0)) +print_rtr(bus1.recv(0)) bus1.send("", 6, rtr=True) -print(bus1.recv(0)) +print_rtr(bus1.recv(0)) bus1.send("", 7, rtr=True) -print(bus1.recv(0)) +print_rtr(bus1.recv(0)) bus1.send("", 16, rtr=True) -print(bus1.any(0)) +print("any", bus1.any(0)) bus1.send("", 32, rtr=True) -print(bus1.recv(0)) +print_rtr(bus1.recv(0)) + +del bus1 # test HAL error, timeout print("==== TEST errors ====") +# Note: this test requires no other CAN node is attached to the CAN peripheral can = pyb.CAN(1, pyb.CAN.NORMAL) try: - can.send("1", 1, timeout=50) + if IS_CLASSIC: + can.send("1", 1, timeout=50) + else: + # Difference between pyb.CAN on Classic vs FDCAN - Classic waits until the message is sent to the bus, + # FDCAN only times out if the TX queue is full + while True: + can.send("1", 1, timeout=50) except OSError as e: - print(repr(e)) + print("timeout", repr(e)) diff --git a/tests/ports/stm32/pyb_can.py.exp b/tests/ports/stm32/pyb_can.py.exp new file mode 100644 index 00000000000..80f44981231 --- /dev/null +++ b/tests/ports/stm32/pyb_can.py.exp @@ -0,0 +1,49 @@ +ValueError -1 +ValueError 0 +CAN 1 +ValueError 3 +CAN(1) +True +CAN(1, CAN.LOOPBACK, auto_restart=False) +any False +error_active True +info [0, 0, 0, 0, 0, 0, 0, 0] +any+info True [0, 0, 0, 0, 0, 0, 1, 0] +[123, False, False, 0, b'abcd'] +[2047, False, False, 0, b'abcd'] +[0, False, False, 0, b'abcd'] +overlong passed +[42, False, False, 0, ] 0 bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') +[42, False, False, 0, ] 4 bytearray(b'1234\x00\x00\x00\x00\x00\x00') +[42, False, False, 0, ] 8 bytearray(b'01234567\x00\x00') +[42, False, False, 0, ] 3 bytearray(b'abc34567\x00\x00') +b'abc' +b'def' +TypeError +ValueError +TypeError +ValueError +ValueError +==== TEST extframe=True ==== +CAN(1, CAN.LOOPBACK, auto_restart=False) +extframe passed +('0x8', '0x1c', '0xa', True, b'ok') +('0x800', '0x1c00', '0xa00', True, b'ok') +('0x80000', '0x1c0000', '0xa0000', True, b'ok') +('0x8000000', '0x1c000000', '0xa000000', True, b'ok') +==== TEST async send ==== +any False +[1, False, False, 0, b'abcde'] +send fail ok +[2, False, False, 0, b'abcde'] +[3, False, False, 0, b'abcde'] +[4, False, False, 0, b'abcde'] +==== TEST rtr messages ==== +any False +5 False True b'' +6 False True b'' +7 False True b'' +any False +32 False True b'' +==== TEST errors ==== +timeout OSError(110,) diff --git a/tests/ports/stm32/pyb_can2.py b/tests/ports/stm32/pyb_can2.py new file mode 100644 index 00000000000..62ae935357c --- /dev/null +++ b/tests/ports/stm32/pyb_can2.py @@ -0,0 +1,50 @@ +try: + from pyb import CAN + + CAN(2) +except (ImportError, ValueError): + print("SKIP") + raise SystemExit + +# Classic CAN (aka bxCAN) hardware has a different filter API +# and some different behaviours to newer FDCAN hardware +IS_CLASSIC = hasattr(CAN, "MASK16") + +# Setting up each CAN peripheral independently is deliberate here, to catch +# catch cases where initialising CAN2 breaks CAN1 + +can1 = CAN(1, CAN.LOOPBACK) +if IS_CLASSIC: + can1.setfilter(0, CAN.LIST16, 0, (123, 124, 125, 126)) +else: + can1.setfilter(0, CAN.RANGE, 0, (123, 126)) + +can2 = CAN(2, CAN.LOOPBACK) +if IS_CLASSIC: + can2.setfilter(0, CAN.LIST16, 0, (3, 4, 5, 6)) +else: + can2.setfilter(0, CAN.RANGE, 0, (3, 6)) + +# Drain any old messages in RX FIFOs +for can in (can1, can2): + while can.any(0): + can.recv(0) + +for id, can in ((1, can1), (2, can2)): + print("testing", id) + # message1 should only receive on can1, message2 on can2 + can.send("message1", 123) + can.send("message2", 3) + did_recv = False + try: + while True: + res = can.recv(0, timeout=50) + # not printing all of 'res' as the filter index result is different + # on Classic vs FD-CAN + print("rx", res[0], res[4]) + did_recv = True + except OSError: + if not did_recv: + print("no rx!") + +print("done") diff --git a/tests/ports/stm32/pyb_can2.py.exp b/tests/ports/stm32/pyb_can2.py.exp new file mode 100644 index 00000000000..9696f2fa010 --- /dev/null +++ b/tests/ports/stm32/pyb_can2.py.exp @@ -0,0 +1,5 @@ +testing 1 +rx 123 b'message1' +testing 2 +rx 3 b'message2' +done diff --git a/tests/ports/stm32/pyb_can_classic_rtr_filter.py b/tests/ports/stm32/pyb_can_classic_rtr_filter.py new file mode 100644 index 00000000000..90ae28ca9be --- /dev/null +++ b/tests/ports/stm32/pyb_can_classic_rtr_filter.py @@ -0,0 +1,30 @@ +try: + from pyb import CAN +except ImportError: + print("SKIP") + raise SystemExit + +if not hasattr(CAN, "LIST16"): + # This test relies on the RTR-aware filters of Classic CAN, + # so needs to be skipped on FDCAN hardware + print("SKIP") + raise SystemExit + +can = CAN(1, CAN.LOOPBACK) +while can.any(0): + can.recv(0) +can.setfilter(0, CAN.LIST32, 0, (1, 2), rtr=(True, True), extframe=True) +can.setfilter(1, CAN.LIST32, 0, (3, 4), rtr=(True, False), extframe=True) +can.setfilter(2, CAN.MASK32, 0, (16, 16), rtr=(False,), extframe=True) +can.setfilter(2, CAN.MASK32, 0, (32, 32), rtr=(True,), extframe=True) + +can.send("", 1, rtr=True, extframe=True) +print(can.recv(0)) +can.send("", 2, rtr=True, extframe=True) +print(can.recv(0)) +can.send("", 3, rtr=True, extframe=True) +print(can.recv(0)) +can.send("", 4, rtr=True, extframe=True) +print(can.any(0)) + +del can diff --git a/tests/ports/stm32/pyb_can_classic_rtr_filter.py.exp b/tests/ports/stm32/pyb_can_classic_rtr_filter.py.exp new file mode 100644 index 00000000000..b970a0509fe --- /dev/null +++ b/tests/ports/stm32/pyb_can_classic_rtr_filter.py.exp @@ -0,0 +1,4 @@ +[1, True, True, 0, b''] +[2, True, True, 1, b''] +[3, True, True, 2, b''] +False diff --git a/tests/ports/stm32/pyb_can_classic_rx.py b/tests/ports/stm32/pyb_can_classic_rx.py new file mode 100644 index 00000000000..39a5eadca54 --- /dev/null +++ b/tests/ports/stm32/pyb_can_classic_rx.py @@ -0,0 +1,89 @@ +try: + from pyb import CAN +except ImportError: + print("SKIP") + raise SystemExit + +if not hasattr(CAN, "LIST16"): + # This test relies on some specific behaviours of the Classic CAN RX FIFO + # interrupt, so needs to be skipped on FDCAN hardware + print("SKIP") + raise SystemExit + +# Test RxCallbacks +print("==== TEST rx callbacks ====") + +can = CAN(1, CAN.LOOPBACK) +can.setfilter(0, CAN.LIST16, 0, (1, 2, 3, 4)) +can.setfilter(1, CAN.LIST16, 1, (5, 6, 7, 8)) + + +def cb0(bus, reason): + print("cb0") + if reason == 0: + print("pending") + if reason == 1: + print("full") + if reason == 2: + print("overflow") + + +def cb1(bus, reason): + print("cb1") + if reason == 0: + print("pending") + if reason == 1: + print("full") + if reason == 2: + print("overflow") + + +def cb0a(bus, reason): + print("cb0a") + if reason == 0: + print("pending") + if reason == 1: + print("full") + if reason == 2: + print("overflow") + + +def cb1a(bus, reason): + print("cb1a") + if reason == 0: + print("pending") + if reason == 1: + print("full") + if reason == 2: + print("overflow") + + +can.rxcallback(0, cb0) +can.rxcallback(1, cb1) + +can.send("11111111", 1, timeout=5000) +can.send("22222222", 2, timeout=5000) +can.send("33333333", 3, timeout=5000) +can.rxcallback(0, cb0a) +can.send("44444444", 4, timeout=5000) + +can.send("55555555", 5, timeout=5000) +can.send("66666666", 6, timeout=5000) +can.send("77777777", 7, timeout=5000) +can.rxcallback(1, cb1a) +can.send("88888888", 8, timeout=5000) + +print(can.recv(0)) +print(can.recv(0)) +print(can.recv(0)) +print(can.recv(1)) +print(can.recv(1)) +print(can.recv(1)) + +can.send("11111111", 1, timeout=5000) +can.send("55555555", 5, timeout=5000) + +print(can.recv(0)) +print(can.recv(1)) + +del can diff --git a/tests/ports/stm32/pyb_can_classic_rx.py.exp b/tests/ports/stm32/pyb_can_classic_rx.py.exp new file mode 100644 index 00000000000..c1c743c42b5 --- /dev/null +++ b/tests/ports/stm32/pyb_can_classic_rx.py.exp @@ -0,0 +1,25 @@ +==== TEST rx callbacks ==== +cb0 +pending +cb0 +full +cb0a +overflow +cb1 +pending +cb1 +full +cb1a +overflow +[1, False, False, 0, b'11111111'] +[2, False, False, 1, b'22222222'] +[4, False, False, 3, b'44444444'] +[5, False, False, 0, b'55555555'] +[6, False, False, 1, b'66666666'] +[8, False, False, 3, b'88888888'] +cb0a +pending +cb1a +pending +[1, False, False, 0, b'11111111'] +[5, False, False, 0, b'55555555'] diff --git a/tests/ports/stm32/rtc.py b/tests/ports/stm32/rtc.py index 013b2f33142..03ed93adc26 100644 --- a/tests/ports/stm32/rtc.py +++ b/tests/ports/stm32/rtc.py @@ -1,13 +1,15 @@ -import pyb, stm +import time, stm from pyb import RTC +prediv_a = stm.mem32[stm.RTC + stm.RTC_PRER] >> 16 + rtc = RTC() rtc.init() print(rtc) # make sure that 1 second passes correctly rtc.datetime((2014, 1, 1, 1, 0, 0, 0, 0)) -pyb.delay(1002) +time.sleep_ms(1002) print(rtc.datetime()[:7]) @@ -38,8 +40,12 @@ def set_and_print(datetime): def set_and_print_calib(cal): - rtc.calibration(cal) - print(rtc.calibration()) + if cal > 0 and prediv_a < 3: + # can't set positive calibration if prediv_a<3, so just make test pass + print(cal) + else: + rtc.calibration(cal) + print(rtc.calibration()) set_and_print_calib(512) diff --git a/tests/ports/stm32/servo.py b/tests/ports/stm32/servo.py index d15cafe483e..0784f64d336 100644 --- a/tests/ports/stm32/servo.py +++ b/tests/ports/stm32/servo.py @@ -1,4 +1,8 @@ -from pyb import Servo +try: + from pyb import Servo +except ImportError: + print("SKIP") + raise SystemExit servo = Servo(1) print(servo) diff --git a/tests/ports/stm32/timer.py b/tests/ports/stm32/timer.py index 251a06c081b..add8c299377 100644 --- a/tests/ports/stm32/timer.py +++ b/tests/ports/stm32/timer.py @@ -1,10 +1,15 @@ # check basic functionality of the timer class -import pyb +import sys from pyb import Timer -tim = Timer(4) -tim = Timer(4, prescaler=100, period=200) +if "STM32WB" in sys.implementation._machine: + tim_id = 16 +else: + tim_id = 4 + +tim = Timer(tim_id) +tim = Timer(tim_id, prescaler=100, period=200) print(tim.prescaler()) print(tim.period()) tim.prescaler(300) diff --git a/tests/ports/stm32/timer_callback.py b/tests/ports/stm32/timer_callback.py index 5f170ccde11..4add88ec6a7 100644 --- a/tests/ports/stm32/timer_callback.py +++ b/tests/ports/stm32/timer_callback.py @@ -1,8 +1,14 @@ # check callback feature of the timer class -import pyb +import sys +import time from pyb import Timer +if "STM32WB" in sys.implementation._machine: + tim_extra_id = 16 +else: + tim_extra_id = 4 + # callback function that disables the callback when called def cb1(t): @@ -29,27 +35,27 @@ def cb4(t): # create a timer with a callback, using callback(None) to stop tim = Timer(1, freq=100, callback=cb1) -pyb.delay(5) +time.sleep_ms(5) print("before cb1") -pyb.delay(15) +time.sleep_ms(15) # create a timer with a callback, using deinit to stop tim = Timer(2, freq=100, callback=cb2) -pyb.delay(5) +time.sleep_ms(5) print("before cb2") -pyb.delay(15) +time.sleep_ms(15) # create a timer, then set the freq, then set the callback -tim = Timer(4) +tim = Timer(tim_extra_id) tim.init(freq=100) tim.callback(cb1) -pyb.delay(5) +time.sleep_ms(5) print("before cb1") -pyb.delay(15) +time.sleep_ms(15) # test callback with a closure tim.init(freq=100) tim.callback(cb3(3)) -pyb.delay(5) +time.sleep_ms(5) print("before cb4") -pyb.delay(15) +time.sleep_ms(15) diff --git a/tests/ports/stm32/uart.py b/tests/ports/stm32/uart.py index 53b0ea6ade4..28eb2261b7f 100644 --- a/tests/ports/stm32/uart.py +++ b/tests/ports/stm32/uart.py @@ -1,5 +1,11 @@ +import sys from pyb import UART +if "STM32WB" in sys.implementation._machine: + # UART(1) is usually connected to the REPL on these MCUs. + print("SKIP") + raise SystemExit + # test we can correctly create by id for bus in (-1, 0, 1, 2, 5, 6): try: diff --git a/tests/ports/stm32_hardware/dma_alignment.py b/tests/ports/stm32_hardware/dma_alignment.py deleted file mode 100644 index 1836b25d86d..00000000000 --- a/tests/ports/stm32_hardware/dma_alignment.py +++ /dev/null @@ -1,43 +0,0 @@ -from machine import SPI -# Regression test for DMA for DCache coherency bugs with cache line -# written originally for https://github.com/micropython/micropython/issues/13471 - -# IMPORTANT: This test requires SPI2 MISO (pin Y8 on Pyboard D) to be connected to GND - -SPI_NUM = 2 - -spi = SPI(SPI_NUM, baudrate=5_000_000) -buf = bytearray(1024) -ok = True - -for offs in range(0, len(buf)): - v = memoryview(buf)[offs : offs + 128] - spi.readinto(v, 0xFF) - if not all(b == 0x00 for b in v): - print(offs, v.hex()) - ok = False - -print("Variable offset fixed length " + ("OK" if ok else "FAIL")) - -# this takes around 30s to run, so skipped if already failing -if ok: - for op_len in range(1, 66): - wr = b"\xFF" * op_len - for offs in range(1, len(buf) - op_len - 1): - # Place some "sentinel" values before and after the DMA buffer - before = offs & 0xFF - after = (~offs) & 0xFF - buf[offs - 1] = before - buf[offs + op_len] = after - v = memoryview(buf)[offs : offs + op_len] - spi.write_readinto(wr, v) - if ( - not all(b == 0x00 for b in v) - or buf[offs - 1] != before - or buf[offs + op_len] != after - ): - print(v.hex()) - print(hex(op_len), hex(offs), hex(buf[offs - 1]), hex(buf[offs + op_len])) - ok = False - - print("Variable offset and lengths " + ("OK" if ok else "FAIL")) diff --git a/tests/ports/stm32_hardware/dma_alignment.py.exp b/tests/ports/stm32_hardware/dma_alignment.py.exp deleted file mode 100644 index e890e0081c4..00000000000 --- a/tests/ports/stm32_hardware/dma_alignment.py.exp +++ /dev/null @@ -1,2 +0,0 @@ -Variable offset fixed length OK -Variable offset and lengths OK diff --git a/tests/ports/stm32_hardware/sdcard_dma_align.py b/tests/ports/stm32_hardware/sdcard_dma_align.py new file mode 100644 index 00000000000..4af1ef543e4 --- /dev/null +++ b/tests/ports/stm32_hardware/sdcard_dma_align.py @@ -0,0 +1,188 @@ +# Test DMA read operations when the buffer alignment in RAM varies. +# +# Test requirements: +# A mostly empty FAT formatted SDCard installed in SD socket +import errno +import os +import pyb +import machine +import micropython +import vfs +import unittest + +from micropython import const + +_BLOCK_SZ = const(512) +_OFFS_WIDTH = const(64) # Should be at least the cache line size plus the GC block size +_TEST_BUF_SZ = const(_BLOCK_SZ + _OFFS_WIDTH) + +# More repeats = longer test run, more chance of triggering a cache coherence issue +_REPEATS = const(8) + +MOUNT_POINT = "/sd" +FILE_PATH = "/sd/stm32_align.blk" + +# Skip the whole test if there isn't a mountable SDCard +try: + sd = pyb.SDCard() + vfs.mount(sd, MOUNT_POINT) + vfs.umount(MOUNT_POINT) + del sd +except (OSError, AttributeError): + print("SKIP") + raise SystemExit + + +def verify_contents(buf, silent=False): + # Verify that each byte in 'buf' has the value of its index in the buffer + bad_pos = [] + for i in range(_BLOCK_SZ): + bi = buf[i] + if bi != i & 0xFF: + if silent: + return False + bad_pos.append((i, bi)) + if not bad_pos: + return True + + assert not silent + + print("{} bad readback values in sector:".format(len(bad_pos)), end="") + for i, v in bad_pos: + print(" {:#x}={:#x}".format(i, v), end="") + print() + return False + + +class TestSDAlign(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.sd = pyb.SDCard() + vfs.mount(cls.sd, MOUNT_POINT) + buf = bytearray(_BLOCK_SZ) + try: + with open(FILE_PATH, "rb") as f: + rlen = f.readinto(buf) + if rlen != _BLOCK_SZ: + raise RuntimeError("Unexpected length of temporary file ", FILE_PATH, rlen) + if not verify_contents(buf): + raise RuntimeError("Corrupt test block in temporary file ", FILE_PATH) + except OSError as e: + if e.errno != errno.ENOENT: + raise + + print("Creating new temp file...") + with open(FILE_PATH, "wb") as f: + f.write(bytes(i & 0xFF for i in range(_BLOCK_SZ))) + + # Now look for the sector which holds the temporary file + # (assume is in the first 20MB of the SD Card) + for b in range(1, 40960): + if not cls.sd.readblocks(b, buf): + raise RuntimeError("Failed to call readblocks on SDCard. block:", b) + if verify_contents(buf, True): + print("Temporary file contents found in block {}".format(b)) + cls.block = b + return + + raise RuntimeError( + "Contents of temporary file not found near start of SDCard. Too many files?" + ) + + @classmethod + def tearDownClass(cls): + try: + os.unlink(FILE_PATH) + print("Deleted temp file") + except OSError: + pass + vfs.umount(MOUNT_POINT) + del cls.sd + + def setUp(self): + self.offs = 0 + + @micropython.native + def _test_reads_inner(self, buf, repeats=_REPEATS): + for offs in range(_OFFS_WIDTH): + with self.subTest(offs=offs): + self.offs = offs + slice = memoryview(buf)[offs : offs + _BLOCK_SZ] + assert len(slice) == _BLOCK_SZ + for r in range(repeats): + self.assertTrue( + self.sd.readblocks(self.block, slice), + "Read failed for block {} offs {} repeat {}/{}".format( + self.block, offs, r, repeats + ), + ) + self.assertTrue( + verify_contents(slice), + "Verify failed for block {} offs {} repeat {}/{}".format( + self.block, offs, r, repeats + ), + ) + + def test_reads(self): + # Test reading at all available offsets in a buffer + buf = bytearray(_TEST_BUF_SZ) + # This test is the most random as any failure depends on speculative reads while + # the DMA operation is in progress. We run the test more times to increase the chance + # of hitting one of these cases, but even if the issue is present the test only fails + # once per ~250 iterations. The other tests inject explicit reads and writes so they fail + # more or less immediately. + self._test_reads_inner(buf, repeats=_REPEATS * 4) + + def test_interrupted_reads(self): + # Test reading at all available offsets in a buffer, while an interrupt is + # scanning through the whole buffer + buf = bytearray(_TEST_BUF_SZ) + t = None + self.scan = 0 + self.val = None + try: + + @micropython.native + def timer_cb(_): + # Arbitrary read from somewhere in the buffer + self.val = buf[self.scan % _TEST_BUF_SZ] + self.scan += 1 + + t = pyb.Timer(1, freq=30_000, callback=timer_cb, hard=True) + self._test_reads_inner(buf) + finally: + if t: + t.deinit() + # print("scan count", self.scan, self.val) + + def test_interrupted_read_write(self): + # Test reading at all available offsets in a buffer, while an interrupt is + # writing before & after the DMA buffer + buf = bytearray(_TEST_BUF_SZ) + t = None + try: + + @micropython.native + def timer_cb(t): + # Arbitrary CPU write just before and after the buffer, trying to dirty a DMA cache line + # + # Note: we never write into a word overlapping the DMA buffer, because if the buffer is not 4-byte aligned + # sdcard_read_blocks() will do a trick to align it temporarily, and this will race with that trick and + # corrupt the memory. + offs = self.offs + if offs > 3: + buf[offs - 4] = 0x55 + offs += _BLOCK_SZ + if offs < _TEST_BUF_SZ - 3: + buf[offs] = 0x56 + + # pybd SF2 at default CPU frequency can manage >30kHz <40kHz + t = pyb.Timer(1, freq=35_000, callback=timer_cb, hard=True) + self._test_reads_inner(buf) + finally: + if t: + t.deinit() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/ports/stm32_hardware/spi_dma_align.py b/tests/ports/stm32_hardware/spi_dma_align.py new file mode 100644 index 00000000000..1c271d48e72 --- /dev/null +++ b/tests/ports/stm32_hardware/spi_dma_align.py @@ -0,0 +1,59 @@ +# Regression test for DMA for DCache coherency bugs with cache line +# written originally for https://github.com/micropython/micropython/issues/13471 + +# IMPORTANT: This test requires SPI2 MISO (pin Y8 on Pyboard D) to be connected to GND + +import unittest +from machine import SPI + +SPI_NUM = 2 +BAUDRATE = 5_000_000 + + +class Test(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.spi = SPI(SPI_NUM, baudrate=BAUDRATE) + cls.skip_slow_test = False + + def test_variable_offset_fixed_length(self): + buf = bytearray(1024) + for offs in range(0, len(buf)): + v = memoryview(buf)[offs : offs + 128] + self.spi.readinto(v, 0xFF) + ok = all(b == 0x00 for b in v) + if not ok: + print(offs, v.hex()) + self.skip_slow_test = True + self.assertTrue(ok) + + def test_variable_offset_and_lengths(self): + # this takes around 30s to run, so skipped if already failing + if self.skip_slow_test: + self.skipTest("already failing") + + buf = bytearray(1024) + for op_len in range(1, 66): + print(op_len) + wr = b"\xff" * op_len + for offs in range(1, len(buf) - op_len - 1): + # Place some "sentinel" values before and after the DMA buffer + before = offs & 0xFF + after = (~offs) & 0xFF + buf[offs - 1] = before + buf[offs + op_len] = after + v = memoryview(buf)[offs : offs + op_len] + self.spi.write_readinto(wr, v) + ok = ( + all(b == 0x00 for b in v) + and buf[offs - 1] == before + and buf[offs + op_len] == after + ) + if not ok: + print(v.hex()) + print(hex(op_len), hex(offs), hex(buf[offs - 1]), hex(buf[offs + op_len])) + self.assertTrue(ok) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/ports/unix/extra_coverage.py b/tests/ports/unix/extra_coverage.py index ec68a555081..2087e7ea2ad 100644 --- a/tests/ports/unix/extra_coverage.py +++ b/tests/ports/unix/extra_coverage.py @@ -6,6 +6,16 @@ import errno import io +import uctypes + +# create an int-like variable used for coverage of `mp_obj_get_ll` +buf = bytearray(b"\xde\xad\xbe\xef") +struct = uctypes.struct( + uctypes.addressof(buf), + {"f32": uctypes.UINT32 | 0}, + uctypes.BIG_ENDIAN, +) +deadbeef = struct.f32 data = extra_coverage() @@ -23,6 +33,7 @@ print(stream.read(1)) # read 1 byte encounters non-blocking error print(stream.readline()) # readline encounters non-blocking error print(stream.readinto(bytearray(10))) # readinto encounters non-blocking error +print(stream.readinto1(bytearray(10))) # readinto1 encounters non-blocking error print(stream.write(b"1")) # write encounters non-blocking error print(stream.write1(b"1")) # write1 encounters non-blocking error stream.set_buf(b"123") @@ -38,6 +49,28 @@ stream.set_error(0) print(stream.ioctl(0, bytearray(10))) # successful ioctl call +print("# stream.readinto") + +# stream.readinto will read 3 bytes then try to read more to fill the buffer, +# but on the second attempt will encounter EIO and should raise that error. +stream.set_error(errno.EIO) +stream.set_buf(b"123") +buf = bytearray(4) +try: + stream.readinto(buf) +except OSError as er: + print("OSError", er.errno == errno.EIO) +print(buf) + +# stream.readinto1 will read 3 bytes then should return them immediately, and +# not encounter the EIO. +stream.set_error(errno.EIO) +stream.set_buf(b"123") +buf = bytearray(4) +print(stream.readinto1(buf), buf) + +print("# stream textio") + stream2 = data[3] # is textio print(stream2.read(1)) # read 1 byte encounters non-blocking error with textio stream @@ -95,7 +128,7 @@ print(returns_NULL()) -# test for freeze_mpy +# test for freeze_mpy (importing prints several lines) import frozentest print(frozentest.__file__) diff --git a/tests/ports/unix/extra_coverage.py.exp b/tests/ports/unix/extra_coverage.py.exp index 176db8e9f84..75d885290c0 100644 --- a/tests/ports/unix/extra_coverage.py.exp +++ b/tests/ports/unix/extra_coverage.py.exp @@ -2,18 +2,34 @@ -123 +123 123 -0123 123 -123 -1ABCDEF +123f +123F +7fffffffffffffff +7FFFFFFFFFFFFFFF +18446744073709551615 +789f +789F ab abc ' abc' ' True' 'Tru' false true (null) -2147483648 2147483648 -80000000 -80000000 +8000000f +8000000F abc % +.a . +<%> + + +<43690> +<43690> +<43690> + +<1000.000000> + +<9223372036854775807> # GC 0 0 @@ -29,6 +45,7 @@ m_tracked_head = 0 5 1 6 1 7 1 +1 0 1 1 1 2 1 @@ -38,38 +55,45 @@ m_tracked_head = 0 6 1 7 1 m_tracked_head = 0 +# tracked realloc +grow preserves data: 1 +shrink preserves data: 1 +realloc gc stable: 1 +realloc(NULL) ok: 1 +realloc(ptr, 0) returns NULL: 1 +m_tracked_head after cleanup: 0 # vstr tests sts test tes -RuntimeError: -RuntimeError: +RuntimeError: \$ +RuntimeError: \$ # repl ame__ -port +port \$ -builtins micropython _asyncio _thread -array binascii btree cexample -cmath collections cppexample cryptolib -deflate errno example_package -ffi framebuf gc hashlib -heapq io json machine -math os platform random -re select socket struct +builtins micropython array binascii +btree cexample cmath collections +cppexample cryptolib deflate errno +example_package ffi framebuf +gc hashlib heapq io +json machine marshal math +os platform random re +select socket string struct sys termios time tls -uctypes vfs websocket +uctypes vfs weakref websocket me -micropython machine math +micropython machine marshal math argv atexit byteorder exc_info executable exit getsizeof implementation intern maxsize modules path platform print_exception ps1 -ps2 stderr stdin stdout -tracebacklimit version version_info +ps2 settrace stderr stdin +stdout tracebacklimit version version_info ementation # attrtuple (start=1, stop=2, step=3) @@ -89,6 +113,23 @@ data 12345 6 -1 +0 +1 +0 +0.000000 +deadbeef +c0ffee777c0ffee +deadbeef +0deadbeef +c0ffee +000c0ffee +# list argument helpers +TypeError: \$ +ValueError: \$ +mp_obj_list_ensure same list? 1 +mp_obj_list_optional_arg same list? 1 +mp_obj_list_optional_arg new list len 3 +mp_obj_list_optional_arg new list from NULL len 3 # runtime utils TypeError: unsupported type for __abs__: 'str' TypeError: unsupported types for __divmod__: 'str', 'str' @@ -97,12 +138,10 @@ TypeError: unsupported types for __divmod__: 'str', 'str' 2 OverflowError: overflow converting long int to machine word OverflowError: overflow converting long int to machine word -ValueError: +TypeError: can't convert NoneType to int +TypeError: can't convert NoneType to int +ValueError: \$ Warning: test -# format float -? -+1e+00 -+1e+00 # binary 123 456 @@ -119,9 +158,16 @@ unlocked 1 2 3 -KeyboardInterrupt: -KeyboardInterrupt: +KeyboardInterrupt: \$ +KeyboardInterrupt: \$ 10 +loop +scheduled function +loop +scheduled function +loop +scheduled function +scheduled function # ringbuf 99 0 98 1 @@ -183,11 +229,17 @@ None None None None +None b'123' b'123' b'123' OSError 0 +# stream.readinto +OSError True +bytearray(b'123\x00') +3 bytearray(b'123\x00') +# stream textio None None cpp None @@ -213,7 +265,7 @@ b'\x00\xff' frzmpy4 1 frzmpy4 2 NULL -uPy +interned a long string that is not interned a string that has unicode αβγ chars b'bytes 1234\x01' diff --git a/tests/ports/unix/ffi_float2.py b/tests/ports/unix/ffi_float2.py index bbed6966627..eac6cd106cf 100644 --- a/tests/ports/unix/ffi_float2.py +++ b/tests/ports/unix/ffi_float2.py @@ -29,4 +29,5 @@ def ffi_open(names): for fun in (tgammaf,): for val in (0.5, 1, 1.0, 1.5, 4, 4.0): - print("%.6f" % fun(val)) + # limit to 5 decimals in order to pass with REPR_C with FORMAT_IMPL_BASIC + print("%.5f" % fun(val)) diff --git a/tests/ports/unix/ffi_float2.py.exp b/tests/ports/unix/ffi_float2.py.exp index 58fc6a01acb..4c750e223a3 100644 --- a/tests/ports/unix/ffi_float2.py.exp +++ b/tests/ports/unix/ffi_float2.py.exp @@ -1,6 +1,6 @@ -1.772454 -1.000000 -1.000000 -0.886227 -6.000000 -6.000000 +1.77245 +1.00000 +1.00000 +0.88623 +6.00000 +6.00000 diff --git a/tests/ports/unix/mod_os.py b/tests/ports/unix/mod_os.py index f69fa45b2b2..468f6badd1e 100644 --- a/tests/ports/unix/mod_os.py +++ b/tests/ports/unix/mod_os.py @@ -1,6 +1,9 @@ # This module is not entirely compatible with CPython import os +if not hasattr(os, "getenv"): + print("SKIP") + raise SystemExit os.putenv("TEST_VARIABLE", "TEST_VALUE") diff --git a/tests/ports/unix/time_mktime_localtime.py b/tests/ports/unix/time_mktime_localtime.py index d1c03c103d7..df5d6cda690 100644 --- a/tests/ports/unix/time_mktime_localtime.py +++ b/tests/ports/unix/time_mktime_localtime.py @@ -1,4 +1,8 @@ -import time +try: + import time +except ImportError: + print("SKIP") + raise SystemExit DAYS_PER_MONTH = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] diff --git a/tests/ports/webassembly/asyncio_top_level_await.mjs b/tests/ports/webassembly/asyncio_top_level_await.mjs index 234b7a6ce6f..fedf10d9c5d 100644 --- a/tests/ports/webassembly/asyncio_top_level_await.mjs +++ b/tests/ports/webassembly/asyncio_top_level_await.mjs @@ -88,3 +88,39 @@ print("top-level end") `); console.log("finished"); + +/**********************************************************/ +// Top-level await's on a JavaScript function that throws. + +console.log("= TEST 4 =========="); + +globalThis.jsFail = async () => { + console.log("jsFail"); + throw new Error("jsFail"); +}; + +await mp.runPythonAsync(` +import asyncio +import js + +# Test top-level catching from a failed JS await. +try: + await js.jsFail() +except Exception as er: + print("caught exception:", type(er), type(er.args[0]), er.args[1:]) + +async def main(): + try: + await js.jsFail() + except Exception as er: + print("caught exception:", type(er), type(er.args[0]), er.args[1:]) + +# Test top-level waiting on a coro that catches. +await main() + +# Test top-level waiting on a task that catches. +t = asyncio.create_task(main()) +await t +`); + +console.log("finished"); diff --git a/tests/ports/webassembly/asyncio_top_level_await.mjs.exp b/tests/ports/webassembly/asyncio_top_level_await.mjs.exp index 66fefd2dcef..f6720b5aba7 100644 --- a/tests/ports/webassembly/asyncio_top_level_await.mjs.exp +++ b/tests/ports/webassembly/asyncio_top_level_await.mjs.exp @@ -17,3 +17,11 @@ top-level wait task task end top-level end finished += TEST 4 ========== +jsFail +caught exception: ('Error', 'jsFail') +jsFail +caught exception: ('Error', 'jsFail') +jsFail +caught exception: ('Error', 'jsFail') +finished diff --git a/tests/ports/webassembly/fun_call.mjs b/tests/ports/webassembly/fun_call.mjs index 295745d2e2e..3a24d61bb93 100644 --- a/tests/ports/webassembly/fun_call.mjs +++ b/tests/ports/webassembly/fun_call.mjs @@ -15,3 +15,30 @@ js.f(1, 2, 3, 4) js.f(1, 2, 3, 4, 5) js.f(1, 2, 3, 4, 5, 6) `); + +globalThis.g = (...args) => { + console.log(args); +}; +mp.runPython(` +import js +js.g() +js.g(a=1) +js.g(a=1, b=2) +js.g(a=1, b=2, c=3) +js.g(a=1, b=2, c=3, d=4) +js.g(a=1, b=2, c=3, d=4, e=5) +js.g(1) +js.g(1, b=2) +js.g(1, b=2, c=3) +js.g(1, b=2, c=3, d=4) +js.g(1, b=2, c=3, d=4, e=5) +js.g(1, 2) +js.g(1, 2, c=3) +js.g(1, 2, c=3, d=4) +js.g(1, 2, c=3, d=4, e=5) +js.g(1, 2, 3) +js.g(1, 2, 3, d=4) +js.g(1, 2, 3, d=4, e=5) +js.g(1, 2, 3, 4) +js.g(1, 2, 3, 4, e=5) +`); diff --git a/tests/ports/webassembly/fun_call.mjs.exp b/tests/ports/webassembly/fun_call.mjs.exp index e9ed5f6ddf9..57edb39a47c 100644 --- a/tests/ports/webassembly/fun_call.mjs.exp +++ b/tests/ports/webassembly/fun_call.mjs.exp @@ -5,3 +5,23 @@ undefined undefined undefined undefined undefined 1 2 3 4 undefined 1 2 3 4 5 1 2 3 4 5 +[] +[ { a: 1 } ] +[ { a: 1, b: 2 } ] +[ { a: 1, b: 2, c: 3 } ] +[ { a: 1, b: 2, c: 3, d: 4 } ] +[ { a: 1, b: 2, c: 3, d: 4, e: 5 } ] +[ 1 ] +[ 1, { b: 2 } ] +[ 1, { b: 2, c: 3 } ] +[ 1, { b: 2, c: 3, d: 4 } ] +[ 1, { b: 2, c: 3, d: 4, e: 5 } ] +[ 1, 2 ] +[ 1, 2, { c: 3 } ] +[ 1, 2, { c: 3, d: 4 } ] +[ 1, 2, { c: 3, d: 4, e: 5 } ] +[ 1, 2, 3 ] +[ 1, 2, 3, { d: 4 } ] +[ 1, 2, 3, { d: 4, e: 5 } ] +[ 1, 2, 3, 4 ] +[ 1, 2, 3, 4, { e: 5 } ] diff --git a/tests/ports/webassembly/heap_expand.mjs.exp b/tests/ports/webassembly/heap_expand.mjs.exp index 56341351492..4161fc7eaed 100644 --- a/tests/ports/webassembly/heap_expand.mjs.exp +++ b/tests/ports/webassembly/heap_expand.mjs.exp @@ -1,27 +1,27 @@ -135241328 -135241296 -135241264 -135241232 -135241184 -135241136 -135241056 -135240912 -135240608 -135240080 -135239040 -135236976 -135232864 -135224656 -135208256 -135175472 -135109856 -134978768 -134716608 -135216752 -136217120 -138217808 -142219264 -150222192 +135233568 +135233536 +135233504 +135233472 +135233424 +135233376 +135233296 +135233152 +135232848 +135232320 +135231280 +135229216 +135225104 +135216896 +135200496 +135167712 +135102096 +134971008 +134708848 +135201312 +136186256 +138156160 +142095984 +149975648 1 2 4 diff --git a/tests/ports/webassembly/js_proxy_identity.mjs b/tests/ports/webassembly/js_proxy_identity.mjs new file mode 100644 index 00000000000..ca2f3980af4 --- /dev/null +++ b/tests/ports/webassembly/js_proxy_identity.mjs @@ -0,0 +1,25 @@ +// Test identity of JsProxy when they are the same JavaScript object. + +const mp = await (await import(process.argv[2])).loadMicroPython(); + +mp.runPython(` +import js + +print(js.Object) + +print("Object equality") +print(js.Object == js.Object) +print(js.Object.assign == js.Object.assign) + +print("Object identity") +print(js.Object is js.Object) + +print("Array equality") +print(js.Array == js.Array) +print(js.Array.prototype == js.Array.prototype) +print(js.Array.prototype.push == js.Array.prototype.push) + +print("Array identity") +print(js.Array is js.Array) +print(js.Array.prototype is js.Array.prototype) +`); diff --git a/tests/ports/webassembly/js_proxy_identity.mjs.exp b/tests/ports/webassembly/js_proxy_identity.mjs.exp new file mode 100644 index 00000000000..d8f1ae89139 --- /dev/null +++ b/tests/ports/webassembly/js_proxy_identity.mjs.exp @@ -0,0 +1,13 @@ + +Object equality +True +True +Object identity +True +Array equality +True +True +True +Array identity +True +True diff --git a/tests/ports/webassembly/js_proxy_reuse_free.mjs b/tests/ports/webassembly/js_proxy_reuse_free.mjs new file mode 100644 index 00000000000..ebca86f0bad --- /dev/null +++ b/tests/ports/webassembly/js_proxy_reuse_free.mjs @@ -0,0 +1,48 @@ +// Test reuse of JsProxy references and freeing of JsProxy objects. +// This ensures that a Python-side JsProxy that refers to a JavaScript object retains +// the correct JavaScript object in the case that another JsProxy that refers to the +// same JavaScript object is freed. + +const mp = await (await import(process.argv[2])).loadMicroPython(); + +globalThis.obj = [1, 2]; +globalThis.obj2 = [3, 4]; + +console.log("JS obj:", globalThis.obj); + +mp.runPython(` +import gc +import js + +# Create 2 proxies of the same JS object. +# They should refer to the same underlying JS-side reference. +obj = js.obj +obj_copy = js.obj +print(obj, obj_copy, obj == obj_copy) + +# Print out the object. +js.console.log("Py obj:", obj) + +# Forget obj_copy and trigger a GC when the Python code finishes. +obj_copy = None +gc.collect() +`); + +console.log("JS obj:", globalThis.obj); + +mp.runPython(` +# Create a new proxy of a different object. +# It should not clobber the existing obj proxy reference. +obj2 = js.obj2 + +# Create a copy of the existing obj proxy. +obj_copy = js.obj + +# Print the JS proxy, it should be the same reference as before. +print(obj, obj_copy, obj == obj_copy) + +# Print out the object. +js.console.log("Py obj:", obj) +`); + +console.log("JS obj:", globalThis.obj); diff --git a/tests/ports/webassembly/js_proxy_reuse_free.mjs.exp b/tests/ports/webassembly/js_proxy_reuse_free.mjs.exp new file mode 100644 index 00000000000..c74e4f49ee4 --- /dev/null +++ b/tests/ports/webassembly/js_proxy_reuse_free.mjs.exp @@ -0,0 +1,7 @@ +JS obj: [ 1, 2 ] + True +Py obj: [ 1, 2 ] +JS obj: [ 1, 2 ] + True +Py obj: [ 1, 2 ] +JS obj: [ 1, 2 ] diff --git a/tests/ports/webassembly/method_bind_behaviour.mjs b/tests/ports/webassembly/method_bind_behaviour.mjs new file mode 100644 index 00000000000..24de0fa3bb1 --- /dev/null +++ b/tests/ports/webassembly/method_bind_behaviour.mjs @@ -0,0 +1,43 @@ +// Test how JavaScript binds self/this when methods are called from Python. + +const mp = await (await import(process.argv[2])).loadMicroPython(); + +// Test accessing and calling JavaScript methods from Python. +mp.runPython(` +import js + +# Get the push method to call later on. +push = js.Array.prototype.push + +# Create initial array. +ar = js.Array(1, 2) +js.console.log(ar) + +# Add an element using a method (should implicitly supply "ar" as context). +print(ar.push(3)) +js.console.log(ar) + +# Add an element using prototype function, need to explicitly provide "ar" as context. +print(push.call(ar, 4)) +js.console.log(ar) + +# Add an element using a method with call and explicit context. +print(ar.push.call(ar, 5)) +js.console.log(ar) + +# Add an element using a different instances method with call and explicit context. +print(js.Array().push.call(ar, 6)) +js.console.log(ar) +`); + +// Test assigning Python functions to JavaScript objects, and using them like a method. +mp.runPython(` +import js + +a = js.Object() +a.meth1 = lambda *x: print("meth1", x) +a.meth1(1, 2) + +js.Object.prototype.meth2 = lambda *x: print("meth2", x) +a.meth2(3, 4) +`); diff --git a/tests/ports/webassembly/method_bind_behaviour.mjs.exp b/tests/ports/webassembly/method_bind_behaviour.mjs.exp new file mode 100644 index 00000000000..ab3743f6672 --- /dev/null +++ b/tests/ports/webassembly/method_bind_behaviour.mjs.exp @@ -0,0 +1,11 @@ +[ 1, 2 ] +3 +[ 1, 2, 3 ] +4 +[ 1, 2, 3, 4 ] +5 +[ 1, 2, 3, 4, 5 ] +6 +[ 1, 2, 3, 4, 5, 6 ] +meth1 (1, 2) +meth2 (3, 4) diff --git a/tests/ports/webassembly/py_proxy_get.mjs b/tests/ports/webassembly/py_proxy_get.mjs new file mode 100644 index 00000000000..825de7cabeb --- /dev/null +++ b/tests/ports/webassembly/py_proxy_get.mjs @@ -0,0 +1,14 @@ +// Test ` get ` on the JavaScript side, which tests PyProxy.get. + +const mp = await (await import(process.argv[2])).loadMicroPython(); + +mp.runPython(` +x = {"a": 1} +`); + +const x = mp.globals.get("x"); +console.log(x.a === 1); +console.log(x.b === undefined); +console.log(typeof x[Symbol.iterator] === "function"); +console.log(x[Symbol.toStringTag] === undefined); +console.log(x.then === undefined); diff --git a/tests/ports/webassembly/py_proxy_get.mjs.exp b/tests/ports/webassembly/py_proxy_get.mjs.exp new file mode 100644 index 00000000000..36c7afad66a --- /dev/null +++ b/tests/ports/webassembly/py_proxy_get.mjs.exp @@ -0,0 +1,5 @@ +true +true +true +true +true diff --git a/tests/ports/webassembly/py_proxy_has.mjs b/tests/ports/webassembly/py_proxy_has.mjs index 8881776fdbe..37df0ae1794 100644 --- a/tests/ports/webassembly/py_proxy_has.mjs +++ b/tests/ports/webassembly/py_proxy_has.mjs @@ -9,3 +9,5 @@ x = [] const x = mp.globals.get("x"); console.log("no_exist" in x); console.log("sort" in x); +console.log(Symbol.toStringTag in x); +console.log(Symbol.iterator in x); diff --git a/tests/ports/webassembly/py_proxy_has.mjs.exp b/tests/ports/webassembly/py_proxy_has.mjs.exp index 1d474d52557..7565230c01f 100644 --- a/tests/ports/webassembly/py_proxy_has.mjs.exp +++ b/tests/ports/webassembly/py_proxy_has.mjs.exp @@ -1,2 +1,4 @@ false true +false +true diff --git a/tests/ports/webassembly/py_proxy_identity.mjs.exp b/tests/ports/webassembly/py_proxy_identity.mjs.exp index 01ccf0d8926..b5b8b210bb6 100644 --- a/tests/ports/webassembly/py_proxy_identity.mjs.exp +++ b/tests/ports/webassembly/py_proxy_identity.mjs.exp @@ -1,3 +1,3 @@ PyProxy { _ref: 3 } PyProxy { _ref: 3 } true -callback +callback diff --git a/tests/ports/webassembly/register_js_module.js b/tests/ports/webassembly/register_js_module.js index b512f2c0dd4..f58d12e50d3 100644 --- a/tests/ports/webassembly/register_js_module.js +++ b/tests/ports/webassembly/register_js_module.js @@ -1,6 +1,45 @@ +// Test the registerJsModule() public API method. + import(process.argv[2]).then((mp) => { mp.loadMicroPython().then((py) => { + // Simple module. py.registerJsModule("js_module", { y: 2 }); py.runPython("import js_module; print(js_module); print(js_module.y)"); + + // Module with functions. + // In particular test how "this" behaves. + py.registerJsModule("js_module2", { + yes: true, + add1(x) { + return x + 1; + }, + getThis() { + return this; + }, + }); + + console.log("===="); + + // Test using simple import. + py.runPython(` +import js_module2 + +print(js_module2.yes) +print(js_module2.add1(1)) +print(js_module2.getThis()) +print(js_module2.getThis().yes) +`); + + console.log("===="); + + // Test using "from ... import". + py.runPython(` +from js_module2 import yes, add1, getThis + +print(yes) +print(add1(2)) +print(getThis()) +print(getThis().yes) +`); }); }); diff --git a/tests/ports/webassembly/register_js_module.js.exp b/tests/ports/webassembly/register_js_module.js.exp index 6e2bad3ce56..34bfd345d17 100644 --- a/tests/ports/webassembly/register_js_module.js.exp +++ b/tests/ports/webassembly/register_js_module.js.exp @@ -1,2 +1,12 @@ 2 +==== +True +2 + +True +==== +True +3 + +True diff --git a/tests/ports/webassembly/run_python_async.mjs.exp b/tests/ports/webassembly/run_python_async.mjs.exp index ad6c49e336e..4dff64a6053 100644 --- a/tests/ports/webassembly/run_python_async.mjs.exp +++ b/tests/ports/webassembly/run_python_async.mjs.exp @@ -2,16 +2,16 @@ 1 py 1 - + py 2 2 resolved 123 3 = TEST 2 ========== 1 - + py 1 - + py 2 2 setTimeout resolved diff --git a/tests/ports/webassembly/weakref_finalize_collect.mjs b/tests/ports/webassembly/weakref_finalize_collect.mjs new file mode 100644 index 00000000000..1e0bc951350 --- /dev/null +++ b/tests/ports/webassembly/weakref_finalize_collect.mjs @@ -0,0 +1,86 @@ +// Test weakref.finalize() functionality requiring gc.collect(). +// Should be kept in sync with tests/basics/weakref_finalize_collect.py. +// +// This needs custom testing on the webassembly port since the GC can only +// run when Python code returns to JavaScript. + +const mp = await (await import(process.argv[2])).loadMicroPython(); + +// Set up. +mp.runPython(` +import gc, weakref + +class A: + def __str__(self): + return "" + +def callback(*args, **kwargs): + print("callback({}, {})".format(args, kwargs)) + return 42 +`); + +console.log("test basic use of finalize() with a simple callback"); +mp.runPython(` + a = A() + f = weakref.finalize(a, callback) + a = None + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print("alive", f.alive) + print("peek", f.peek()) + print("detach", f.detach()) + print("call", f()) +`); + +console.log("test that a callback is passed the correct values"); +mp.runPython(` + a = A() + f = weakref.finalize(a, callback, 1, 2, kwarg=3) + a = None + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print("alive", f.alive) + print("peek", f.peek()) + print("detach", f.detach()) + print("call", f()) +`); + +console.log("test that calling the finalizer cancels the finalizer"); +mp.runPython(` + a = A() + f = weakref.finalize(a, callback) + print(f()) + print(a) + a = None + gc.collect() +`); +console.log("(outside Python)"); + +console.log("test that calling detach cancels the finalizer"); +mp.runPython(` + a = A() + f = weakref.finalize(a, callback) + print(len(f.detach())) + print(a) + a = None + gc.collect() +`); +console.log("(outside Python)"); + +console.log("test that finalize does not get collected before its ref does"); +mp.runPython(` + a = A() + weakref.finalize(a, callback) + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print("free a") + a = None + gc.collect() +`); +console.log("(outside Python)"); diff --git a/tests/ports/webassembly/weakref_finalize_collect.mjs.exp b/tests/ports/webassembly/weakref_finalize_collect.mjs.exp new file mode 100644 index 00000000000..e8087a4ae9b --- /dev/null +++ b/tests/ports/webassembly/weakref_finalize_collect.mjs.exp @@ -0,0 +1,28 @@ +test basic use of finalize() with a simple callback +callback((), {}) +(outside Python) +alive False +peek None +detach None +call None +test that a callback is passed the correct values +callback((1, 2), {'kwarg': 3}) +(outside Python) +alive False +peek None +detach None +call None +test that calling the finalizer cancels the finalizer +callback((), {}) +42 + +(outside Python) +test that calling detach cancels the finalizer +4 + +(outside Python) +test that finalize does not get collected before its ref does +(outside Python) +free a +callback((), {}) +(outside Python) diff --git a/tests/ports/webassembly/weakref_ref_collect.mjs b/tests/ports/webassembly/weakref_ref_collect.mjs new file mode 100644 index 00000000000..546a851f0ac --- /dev/null +++ b/tests/ports/webassembly/weakref_ref_collect.mjs @@ -0,0 +1,69 @@ +// Test weakref.ref() functionality requiring gc.collect(). +// Should be kept in sync with tests/basics/weakref_ref_collect.py. +// +// This needs custom testing on the webassembly port since the GC can only +// run when Python code returns to JavaScript. + +const mp = await (await import(process.argv[2])).loadMicroPython(); + +// Set up. +mp.runPython(` +import gc, weakref + +class A: + def __str__(self): + return "" + +def callback(r): + print("callback", r()) +`); + +console.log("test basic use of ref() with only one argument"); +mp.runPython(` + a = A() + r = weakref.ref(a) + print(r()) + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print(r()) + a = None + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print(r()) +`); + +console.log("test use of ref() with a callback"); +mp.runPython(` + a = A() + r = weakref.ref(a, callback) + print(r()) + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print(r()) + a = None + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print(r()) +`); + +console.log("test when weakref gets collected before the object it refs"); +mp.runPython(` + a = A() + r = weakref.ref(a, callback) + print(r()) + r = None + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + a = None + gc.collect() +`); diff --git a/tests/ports/webassembly/weakref_ref_collect.mjs.exp b/tests/ports/webassembly/weakref_ref_collect.mjs.exp new file mode 100644 index 00000000000..f903d417028 --- /dev/null +++ b/tests/ports/webassembly/weakref_ref_collect.mjs.exp @@ -0,0 +1,16 @@ +test basic use of ref() with only one argument + +(outside Python) + +(outside Python) +None +test use of ref() with a callback + +(outside Python) + +callback None +(outside Python) +None +test when weakref gets collected before the object it refs + +(outside Python) diff --git a/tests/run-internalbench.py b/tests/run-internalbench.py index c9f783e474c..715b0ffc9ed 100755 --- a/tests/run-internalbench.py +++ b/tests/run-internalbench.py @@ -8,48 +8,92 @@ from glob import glob from collections import defaultdict +from test_utils import ( + base_path, + pyboard, + test_instance_description, + test_instance_epilog, + test_directory_description, + get_test_instance, +) + if os.name == "nt": MICROPYTHON = os.getenv( "MICROPY_MICROPYTHON", "../ports/windows/build-standard/micropython.exe" ) + CPYTHON3 = os.getenv("MICROPY_CPYTHON3", "python3") else: MICROPYTHON = os.getenv("MICROPY_MICROPYTHON", "../ports/unix/build-standard/micropython") + CPYTHON3 = os.getenv("MICROPY_CPYTHON3", "python3") + +MICROPYTHON_CMD = [MICROPYTHON, "-X", "emit=bytecode"] +CPYTHON3_CMD = [CPYTHON3, "-BS"] + + +injected_bench_code = b""" +import time +class bench_class: + ITERS = 20000000 + + @staticmethod + def run(test): + t = time.ticks_us() + test(bench_class.ITERS) + t = time.ticks_diff(time.ticks_us(), t) + s, us = divmod(t, 1_000_000) + print("{}.{:06}".format(s, us)) + +import sys +sys.modules['bench'] = bench_class +""" -def run_tests(pyb, test_dict): + +def execbench(test_instance, filename, iters): + with open(filename, "rb") as f: + pyfile = f.read() + code = (injected_bench_code + pyfile).replace(b"20000000", str(iters).encode("utf-8")) + return test_instance.exec(code).replace(b"\r\n", b"\n") + + +def run_tests(test_instance, test_dict, iters): test_count = 0 testcase_count = 0 for base_test, tests in sorted(test_dict.items()): print(base_test + ":") + baseline = None for test_file in tests: # run MicroPython - if pyb is None: + if isinstance(test_instance, list): # run on PC try: - output_mupy = subprocess.check_output( - [MICROPYTHON, "-X", "emit=bytecode", test_file[0]] - ) + output_mupy = subprocess.check_output(test_instance + [test_file[0]]) except subprocess.CalledProcessError: output_mupy = b"CRASH" else: # run on pyboard - pyb.enter_raw_repl() + test_instance.enter_raw_repl() try: - output_mupy = pyb.execfile(test_file).replace(b"\r\n", b"\n") + output_mupy = execbench(test_instance, test_file[0], iters) except pyboard.PyboardError: output_mupy = b"CRASH" - output_mupy = float(output_mupy.strip()) + try: + output_mupy = float(output_mupy.strip()) + except ValueError: + output_mupy = -1 test_file[1] = output_mupy testcase_count += 1 - test_count += 1 - baseline = None - for t in tests: if baseline is None: - baseline = t[1] - print(" %.3fs (%+06.2f%%) %s" % (t[1], (t[1] * 100 / baseline) - 100, t[0])) + baseline = test_file[1] + print( + " %.3fs (%+06.2f%%) %s" + % (test_file[1], (test_file[1] * 100 / baseline) - 100, test_file[0]) + ) + + test_count += 1 print("{} tests performed ({} individual testcases)".format(test_count, testcase_count)) @@ -58,27 +102,52 @@ def run_tests(pyb, test_dict): def main(): - cmd_parser = argparse.ArgumentParser(description="Run tests for MicroPython.") - cmd_parser.add_argument("--pyboard", action="store_true", help="run the tests on the pyboard") + cmd_parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=f"""Run and manage tests for MicroPython. + +{test_instance_description} +{test_directory_description} +""", + epilog=f"""{test_instance_epilog}- cpython - use CPython to run the benchmarks instead\n""", + ) + cmd_parser.add_argument( + "-t", "--test-instance", default="unix", help="the MicroPython instance to test" + ) + cmd_parser.add_argument( + "-b", "--baudrate", default=115200, help="the baud rate of the serial device" + ) + cmd_parser.add_argument("-u", "--user", default="micro", help="the telnet login username") + cmd_parser.add_argument("-p", "--password", default="python", help="the telnet login password") + cmd_parser.add_argument( + "-d", "--test-dirs", nargs="*", help="input test directories (if no files given)" + ) + cmd_parser.add_argument( + "-I", + "--iters", + type=int, + default=200_000, + help="number of test iterations, only for remote instances (default 200,000)", + ) cmd_parser.add_argument("files", nargs="*", help="input test files") args = cmd_parser.parse_args() - # Note pyboard support is copied over from run-tests.py, not tests, and likely needs revamping - if args.pyboard: - import pyboard - - pyb = pyboard.Pyboard("/dev/ttyACM0") - pyb.enter_raw_repl() + if args.test_instance == "cpython": + test_instance = CPYTHON3_CMD else: - pyb = None + # Note pyboard support is copied over from run-tests.py, not tests, and likely needs revamping + test_instance = get_test_instance( + args.test_instance, args.baudrate, args.user, args.password + ) + if test_instance is None: + test_instance = MICROPYTHON_CMD if len(args.files) == 0: - if pyb is None: - # run PC tests - test_dirs = ("internal_bench",) + if args.test_dirs: + test_dirs = tuple(args.test_dirs) else: - # run pyboard tests - test_dirs = ("basics", "float", "pyb") + test_dirs = ("internal_bench",) + tests = sorted( test_file for test_files in (glob("{}/*.py".format(dir)) for dir in test_dirs) @@ -95,7 +164,7 @@ def main(): continue test_dict[m.group(1)].append([t, None]) - if not run_tests(pyb, test_dict): + if not run_tests(test_instance, test_dict, args.iters): sys.exit(1) diff --git a/tests/run-multitests.py b/tests/run-multitests.py index 93a6d3844d2..40aac16c16f 100755 --- a/tests/run-multitests.py +++ b/tests/run-multitests.py @@ -15,6 +15,14 @@ import subprocess import tempfile +from test_utils import ( + base_path, + pyboard, + test_instance_epilog, + convert_device_shortcut_to_real_device, + create_test_report, +) + test_dir = os.path.abspath(os.path.dirname(__file__)) if os.path.abspath(sys.path[0]) == test_dir: @@ -22,9 +30,6 @@ # accidentally importing tests like micropython/const.py sys.path.pop(0) -sys.path.insert(0, test_dir + "/../tools") -import pyboard - if os.name == "nt": CPYTHON3 = os.getenv("MICROPY_CPYTHON3", "python3.exe") MICROPYTHON = os.path.abspath( @@ -105,15 +110,14 @@ def output_metric(data): multitest.flush() """ -# The btstack implementation on Unix generates some spurious output that we -# can't control. Also other platforms may output certain warnings/errors that -# can be safely ignored. +# Some ports generate output we can't control, and that can be safely ignored. IGNORE_OUTPUT_MATCHES = ( - "libusb: error ", # It tries to open devices that it doesn't have access to (libusb prints unconditionally). + "libusb: error ", # unix btstack tries to open devices that it doesn't have access to (libusb prints unconditionally). "hci_transport_h2_libusb.c", # Same issue. We enable LOG_ERROR in btstack. - "USB Path: ", # Hardcoded in btstack's libusb transport. - "hci_number_completed_packet", # Warning from btstack. + "USB Path: ", # Hardcoded in unix btstack's libusb transport. + "hci_number_completed_packet", # Warning from unix btstack. "lld_pdu_get_tx_flush_nb HCI packet count mismatch (", # From ESP-IDF, see https://github.com/espressif/esp-idf/issues/5105 + " ets_task(", # ESP8266 port debug output ) @@ -131,6 +135,11 @@ def get_host_ip(_ip_cache=[]): return _ip_cache[0] +def decode(output): + # Convenience function to convert raw process or serial output to ASCII + return str(output, "ascii", "backslashreplace") + + class PyInstance: def __init__(self): pass @@ -189,7 +198,7 @@ def run_script(self, script): output = p.stdout except subprocess.CalledProcessError as er: err = er - return str(output.strip(), "ascii"), err + return decode(output.strip()), err def start_script(self, script): self.popen = subprocess.Popen( @@ -216,7 +225,7 @@ def readline(self): self.finished = self.popen.poll() is not None return None, None else: - return str(out.rstrip(), "ascii"), None + return decode(out.rstrip()), None def write(self, data): self.popen.stdin.write(data) @@ -228,21 +237,12 @@ def is_finished(self): def wait_finished(self): self.popen.wait() out = self.popen.stdout.read() - return str(out, "ascii"), "" + return decode(out), "" class PyInstancePyboard(PyInstance): - @staticmethod - def map_device_shortcut(device): - if device[0] == "a" and device[1:].isdigit(): - return "/dev/ttyACM" + device[1:] - elif device[0] == "u" and device[1:].isdigit(): - return "/dev/ttyUSB" + device[1:] - else: - return device - def __init__(self, device): - device = self.map_device_shortcut(device) + device = device self.device = device self.pyb = pyboard.Pyboard(device) self.pyb.enter_raw_repl() @@ -263,7 +263,7 @@ def run_script(self, script): output = self.pyb.exec_(script) except pyboard.PyboardError as er: err = er - return str(output.strip(), "ascii"), err + return decode(output.strip()), err def start_script(self, script): self.pyb.enter_raw_repl() @@ -276,19 +276,22 @@ def stop(self): def readline(self): if self.finished: return None, None - if self.pyb.serial.inWaiting() == 0: - return None, None - out = self.pyb.read_until(1, (b"\r\n", b"\x04")) - if out.endswith(b"\x04"): - self.finished = True - out = out[:-1] - err = str(self.pyb.read_until(1, b"\x04"), "ascii") - err = err[:-1] - if not out and not err: + try: + if self.pyb.serial.inWaiting() == 0: return None, None - else: - err = None - return str(out.rstrip(), "ascii"), err + out = self.pyb.read_until(1, (b"\r\n", b"\x04")) + if out.endswith(b"\x04"): + self.finished = True + out = out[:-1] + err = decode(self.pyb.read_until(1, b"\x04")) + err = err[:-1] + if not out and not err: + return None, None + else: + err = None + return decode(out.rstrip()), err + except OSError as e: + return None, "Failed to read from instance: {}".format(e) def write(self, data): self.pyb.serial.write(data) @@ -298,7 +301,7 @@ def is_finished(self): def wait_finished(self): out, err = self.pyb.follow(10, None) - return str(out, "ascii"), str(err, "ascii") + return decode(out), decode(err) def prepare_test_file_list(test_files): @@ -434,7 +437,10 @@ def run_test_on_instances(test_file, num_instances, instances): # Stop all instances for idx in range(num_instances): - instances[idx].stop() + try: + instances[idx].stop() + except OSError as e: + output[idx].append("Runner failed to stop instance: {}".format(e)) output_str = "" for idx, lines in enumerate(output): @@ -489,9 +495,7 @@ def print_diff(a, b): def run_tests(test_files, instances_truth, instances_test): - skipped_tests = [] - passed_tests = [] - failed_tests = [] + test_results = [] for test_file, num_instances in test_files: instances_str = "|".join(str(instances_test[i]) for i in range(num_instances)) @@ -527,13 +531,13 @@ def run_tests(test_files, instances_truth, instances_test): # Print result of test if skip: print("skip") - skipped_tests.append(test_file) + test_results.append((test_file, "skip", "")) elif output_test == output_truth: print("pass") - passed_tests.append(test_file) + test_results.append((test_file, "pass", "")) else: print("FAIL") - failed_tests.append(test_file) + test_results.append((test_file, "fail", "")) if not cmd_args.show_output: print("### TEST ###") print(output_test, end="") @@ -550,15 +554,7 @@ def run_tests(test_files, instances_truth, instances_test): if cmd_args.show_output: print() - print("{} tests performed".format(len(skipped_tests) + len(passed_tests) + len(failed_tests))) - print("{} tests passed".format(len(passed_tests))) - - if skipped_tests: - print("{} tests skipped: {}".format(len(skipped_tests), " ".join(skipped_tests))) - if failed_tests: - print("{} tests failed: {}".format(len(failed_tests), " ".join(failed_tests))) - - return not failed_tests + return test_results def main(): @@ -566,16 +562,24 @@ def main(): cmd_parser = argparse.ArgumentParser( description="Run network tests for MicroPython", + epilog=( + test_instance_epilog + + "Each instance arg can optionally have custom env provided, eg. ,ENV=VAR,ENV=VAR...\n" + ), formatter_class=argparse.RawTextHelpFormatter, ) cmd_parser.add_argument( "-s", "--show-output", action="store_true", help="show test output after running" ) cmd_parser.add_argument( - "-t", "--trace-output", action="store_true", help="trace test output while running" + "-c", "--trace-output", action="store_true", help="trace test output while running" ) cmd_parser.add_argument( - "-i", "--instance", action="append", default=[], help="instance(s) to run the tests on" + "-t", + "--test-instance", + action="append", + default=[], + help="instance(s) to run the tests on", ) cmd_parser.add_argument( "-p", @@ -584,13 +588,11 @@ def main(): default=1, help="repeat the test with this many permutations of the instance order", ) - cmd_parser.epilog = ( - "Supported instance types:\r\n" - " -i pyb: physical device (eg. pyboard) on provided repl port.\n" - " -i micropython unix micropython instance, path customised with MICROPY_MICROPYTHON env.\n" - " -i cpython desktop python3 instance, path customised with MICROPY_CPYTHON3 env.\n" - " -i exec: custom program run on provided path.\n" - "Each instance arg can optionally have custom env provided, eg. ,ENV=VAR,ENV=VAR...\n" + cmd_parser.add_argument( + "-r", + "--result-dir", + default=base_path("results"), + help="directory for test results", ) cmd_parser.add_argument("files", nargs="+", help="input test files") cmd_args = cmd_parser.parse_args() @@ -604,33 +606,36 @@ def main(): instances_truth = [PyInstanceSubProcess([PYTHON_TRUTH]) for _ in range(max_instances)] instances_test = [] - for i in cmd_args.instance: + for i in cmd_args.test_instance: # Each instance arg is ,ENV=VAR,ENV=VAR... i = i.split(",") cmd = i[0] env = i[1:] if cmd.startswith("exec:"): instances_test.append(PyInstanceSubProcess([cmd[len("exec:") :]], env)) - elif cmd == "micropython": + elif cmd == "unix": instances_test.append(PyInstanceSubProcess([MICROPYTHON], env)) elif cmd == "cpython": instances_test.append(PyInstanceSubProcess([CPYTHON3], env)) - elif cmd.startswith("pyb:"): - instances_test.append(PyInstancePyboard(cmd[len("pyb:") :])) + elif cmd == "webassembly" or cmd.startswith("execpty:"): + print("unsupported instance string: {}".format(cmd), file=sys.stderr) + sys.exit(2) else: - print("unknown instance string: {}".format(cmd), file=sys.stderr) - sys.exit(1) + device = convert_device_shortcut_to_real_device(cmd) + instances_test.append(PyInstancePyboard(device)) for _ in range(max_instances - len(instances_test)): instances_test.append(PyInstanceSubProcess([MICROPYTHON])) + os.makedirs(cmd_args.result_dir, exist_ok=True) all_pass = True try: for i, instances_test_permutation in enumerate(itertools.permutations(instances_test)): if i >= cmd_args.permutations: break - all_pass &= run_tests(test_files, instances_truth, instances_test_permutation) + test_results = run_tests(test_files, instances_truth, instances_test_permutation) + all_pass &= create_test_report(cmd_args, test_results) finally: for i in instances_truth: diff --git a/tests/run-natmodtests.py b/tests/run-natmodtests.py index f1a2a974690..83974a6aa09 100755 --- a/tests/run-natmodtests.py +++ b/tests/run-natmodtests.py @@ -9,8 +9,15 @@ import sys import argparse -sys.path.append("../tools") -import pyboard +from test_utils import ( + base_path, + pyboard, + TEST_ENTER_RAW_REPL_TIMEOUT, + TEST_MAXIMUM_RAW_REPL_FAILURES, + test_instance_epilog, + get_test_instance, + create_test_report, +) # Paths for host executables CPYTHON3 = os.getenv("MICROPY_CPYTHON3", "python3") @@ -28,6 +35,24 @@ "re": "re/re_$(ARCH).mpy", } +# Supported architectures for native mpy modules +AVAILABLE_ARCHS = ( + "x86", + "x64", + "armv6", + "armv6m", + "armv7m", + "armv7em", + "armv7emsp", + "armv7emdp", + "xtensa", + "xtensawin", + "rv32imc", + "rv64imc", +) + +ARCH_MAPPINGS = {"armv7em": "armv7m"} + # Code to allow a target MicroPython to import an .mpy from RAM injected_import_hook_code = """\ import sys, io, vfs @@ -35,7 +60,9 @@ class __File(io.IOBase): def __init__(self): self.off = 0 def ioctl(self, request, arg): - return 0 + if request == 4: # MP_STREAM_CLOSE + return 0 + return -1 def readinto(self, buf): buf[:] = memoryview(__buf)[self.off:self.off + len(buf)] self.off += len(buf) @@ -54,6 +81,7 @@ def open(self, path, mode): return __File() vfs.mount(__FS(), '/__remote') sys.path.insert(0, '/__remote') +{import_prelude} sys.modules['{}'] = __import__('__injected') """ @@ -86,7 +114,7 @@ def close(self): def run_script(self, script): try: - self.pyb.enter_raw_repl() + self.pyb.enter_raw_repl(timeout_overall=TEST_ENTER_RAW_REPL_TIMEOUT) output = self.pyb.exec_(script) output = output.replace(b"\r\n", b"\n") return output, None @@ -94,14 +122,35 @@ def run_script(self, script): return b"", er -def run_tests(target_truth, target, args, stats): +def detect_architecture(target): + with open("./feature_check/target_info.py", "rb") as f: + target_info_data = f.read() + result_out, error = target.run_script(target_info_data) + if error is not None: + return None, None, error + info = result_out.split(b" ") + if len(info) < 2: + return None, None, "unexpected target info: {}".format(info) + platform = info[0].strip().decode() + arch = info[1].strip().decode() + if arch not in AVAILABLE_ARCHS: + if arch == "None": + return None, None, "the target does not support dynamic modules" + else: + return None, None, "{} is not a supported architecture".format(arch) + return platform, arch, None + + +def run_tests(target_truth, target, args, resolved_arch): + raw_repl_failure_count = 0 + test_results = [] for test_file in args.files: # Find supported test test_file_basename = os.path.basename(test_file) for k, v in TEST_MAPPINGS.items(): if test_file_basename.startswith(k): test_module = k - test_mpy = v.replace("$(ARCH)", args.arch) + test_mpy = v.replace("$(ARCH)", resolved_arch) break else: print("---- {} - no matching mpy".format(test_file)) @@ -117,9 +166,11 @@ def run_tests(target_truth, target, args, stats): with open(NATMOD_EXAMPLE_DIR + test_mpy, "rb") as f: test_script += b"__buf=" + bytes(repr(f.read()), "ascii") + b"\n" except OSError: - print("---- {} - mpy file not compiled".format(test_file)) + test_results.append((test_file, "skip", "mpy file not compiled")) + print("skip {} - mpy file not compiled".format(test_file)) continue test_script += bytes(injected_import_hook_code.format(test_module), "ascii") + test_script += b"print('START TEST')\n" test_script += test_file_data # Run test under MicroPython @@ -127,11 +178,23 @@ def run_tests(target_truth, target, args, stats): # Work out result of test extra = "" + result_out = result_out.removeprefix(b"START TEST\n") if error is None and result_out == b"SKIP\n": result = "SKIP" + elif ( + error is not None + and error.args[0] == "exception" + and error.args[1] == b"" + and b"MemoryError" in error.args[2] + ): + # Test had a MemoryError before anything (should be at least "START TEST") + # was printed, so the test is too big for the target. + result = "LRGE" elif error is not None: result = "FAIL" extra = " - " + str(error) + if str(error).startswith("could not enter raw repl"): + raw_repl_failure_count += 1 else: # Check result against truth try: @@ -149,55 +212,96 @@ def run_tests(target_truth, target, args, stats): result = "pass" # Accumulate statistics - stats["total"] += 1 if result == "pass": - stats["pass"] += 1 + test_results.append((test_file, "pass", "")) elif result == "SKIP": - stats["skip"] += 1 + test_results.append((test_file, "skip", "")) + elif result == "LRGE": + test_results.append((test_file, "skip", "too large")) else: - stats["fail"] += 1 + test_results.append((test_file, "fail", "")) # Print result print("{:4} {}{}".format(result, test_file, extra)) + if raw_repl_failure_count > TEST_MAXIMUM_RAW_REPL_FAILURES: + print("Too many raw REPL failures, aborting test run") + break + + return test_results + def main(): + global injected_import_hook_code + cmd_parser = argparse.ArgumentParser( - description="Run dynamic-native-module tests under MicroPython" + description="Run dynamic-native-module tests under MicroPython", + epilog=test_instance_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + cmd_parser.add_argument( + "-t", "--test-instance", default="unix", help="the MicroPython instance to test" ) + cmd_parser.add_argument("--baudrate", default=115200, help="baud rate of the serial device") + cmd_parser.add_argument("--user", default="micro", help="telnet login username") + cmd_parser.add_argument("--password", default="python", help="telnet login password") cmd_parser.add_argument( - "-p", "--pyboard", action="store_true", help="run tests via pyboard.py" + "-a", "--arch", choices=AVAILABLE_ARCHS, help="override native architecture of the target" ) cmd_parser.add_argument( - "-d", "--device", default="/dev/ttyACM0", help="the device for pyboard.py" + "-b", + "--begin", + metavar="PROLOGUE", + default=None, + help="prologue python file to execute before module import", ) cmd_parser.add_argument( - "-a", "--arch", default="x64", help="native architecture of the target" + "-r", + "--result-dir", + default=base_path("results"), + help="directory for test results", ) cmd_parser.add_argument("files", nargs="*", help="input test files") args = cmd_parser.parse_args() + prologue = "" + if args.begin: + with open(args.begin, "rt") as source: + prologue = source.read() + injected_import_hook_code = injected_import_hook_code.replace("{import_prelude}", prologue) + target_truth = TargetSubprocess([CPYTHON3]) - if args.pyboard: - target = TargetPyboard(pyboard.Pyboard(args.device)) - else: + target = get_test_instance(args.test_instance, args.baudrate, args.user, args.password) + if target is None: + # Use the unix port of MicroPython. target = TargetSubprocess([MICROPYTHON]) + else: + # Use a remote target. + target = TargetPyboard(target) - stats = {"total": 0, "pass": 0, "fail": 0, "skip": 0} - run_tests(target_truth, target, args, stats) + if hasattr(args, "arch") and args.arch is not None: + target_arch = args.arch + target_platform = None + else: + target_platform, target_arch, error = detect_architecture(target) + if error: + print("Cannot run tests: {}".format(error)) + sys.exit(2) + target_arch = ARCH_MAPPINGS.get(target_arch, target_arch) + + if target_platform: + print("platform={} ".format(target_platform), end="") + print("arch={}".format(target_arch)) + + os.makedirs(args.result_dir, exist_ok=True) + test_results = run_tests(target_truth, target, args, target_arch) + res = create_test_report(args, test_results) target.close() target_truth.close() - print("{} tests performed".format(stats["total"])) - print("{} tests passed".format(stats["pass"])) - if stats["fail"]: - print("{} tests failed".format(stats["fail"])) - if stats["skip"]: - print("{} tests skipped".format(stats["skip"])) - - if stats["fail"]: + if not res: sys.exit(1) diff --git a/tests/run-perfbench.py b/tests/run-perfbench.py index 81d873c4599..686c3da7ae4 100755 --- a/tests/run-perfbench.py +++ b/tests/run-perfbench.py @@ -10,10 +10,13 @@ import argparse from glob import glob -sys.path.append("../tools") -import pyboard - -prepare_script_for_target = __import__("run-tests").prepare_script_for_target +from test_utils import ( + base_path, + pyboard, + get_test_instance, + prepare_script_for_target, + create_test_report, +) # Paths for host executables if os.name == "nt": @@ -45,7 +48,7 @@ def run_script_on_target(target, script): output = b"" err = None - if isinstance(target, pyboard.Pyboard): + if hasattr(target, "enter_raw_repl"): # Run via pyboard interface try: target.enter_raw_repl() @@ -90,22 +93,22 @@ def run_benchmark_on_target(target, script): def run_benchmarks(args, target, param_n, param_m, n_average, test_list): + test_results = [] skip_complex = run_feature_test(target, "complex") != "complex" skip_native = run_feature_test(target, "native_check") != "native" - target_had_error = False for test_file in sorted(test_list): print(test_file + ": ", end="") # Check if test should be skipped - skip = ( - skip_complex - and test_file.find("bm_fft") != -1 - or skip_native - and test_file.find("viper_") != -1 - ) - if skip: - print("SKIP") + skip_reason = None + if skip_complex and test_file.endswith(("bm_fft.py", "misc_mandel.py")): + skip_reason = "complex not supported" + elif skip_native and test_file.find("viper_") != -1: + skip_reason = "native not supported" + if skip_reason: + test_results.append((test_file, "skip", skip_reason)) + print("SKIP:", skip_reason) continue # Create test script @@ -122,9 +125,10 @@ def run_benchmarks(args, target, param_n, param_m, n_average, test_list): f.write(test_script) # Process script through mpy-cross if needed - if isinstance(target, pyboard.Pyboard) or args.via_mpy: + if hasattr(target, "enter_raw_repl") or args.via_mpy: crash, test_script_target = prepare_script_for_target(args, script_text=test_script) if crash: + test_results.append((test_file, "fail", "preparation")) print("CRASH:", test_script_target) continue else: @@ -162,10 +166,13 @@ def run_benchmarks(args, target, param_n, param_m, n_average, test_list): error = "FAIL truth" if error is not None: - if not error.startswith("SKIP"): - target_had_error = True + if error.startswith("SKIP"): + test_results.append((test_file, "skip", error)) + else: + test_results.append((test_file, "fail", error)) print(error) else: + test_results.append((test_file, "pass", "")) t_avg, t_sd = compute_stats(times) s_avg, s_sd = compute_stats(scores) print( @@ -179,7 +186,7 @@ def run_benchmarks(args, target, param_n, param_m, n_average, test_list): sys.stdout.flush() - return target_had_error + return test_results def parse_output(filename): @@ -190,7 +197,13 @@ def parse_output(filename): m = int(m.split("=")[1]) data = [] for l in f: - if ": " in l and ": SKIP" not in l and "CRASH: " not in l: + if ( + ": " in l + and ": SKIP" not in l + and "CRASH: " not in l + and "skipped: " not in l + and "failed: " not in l + ): name, values = l.strip().split(": ") values = tuple(float(v) for v in values.split()) data.append((name,) + values) @@ -246,17 +259,17 @@ def compute_diff(file1, file2, diff_score): def main(): cmd_parser = argparse.ArgumentParser(description="Run benchmarks for MicroPython") cmd_parser.add_argument( - "-t", "--diff-time", action="store_true", help="diff time outputs from a previous run" + "-m", "--diff-time", action="store_true", help="diff time outputs from a previous run" ) cmd_parser.add_argument( "-s", "--diff-score", action="store_true", help="diff score outputs from a previous run" ) cmd_parser.add_argument( - "-p", "--pyboard", action="store_true", help="run tests via pyboard.py" - ) - cmd_parser.add_argument( - "-d", "--device", default="/dev/ttyACM0", help="the device for pyboard.py" + "-t", "--test-instance", default="unix", help="the MicroPython instance to test" ) + cmd_parser.add_argument("--baudrate", default=115200, help="baud rate of the serial device") + cmd_parser.add_argument("--user", default="micro", help="telnet login username") + cmd_parser.add_argument("--password", default="python", help="telnet login password") cmd_parser.add_argument("-a", "--average", default="8", help="averaging number") cmd_parser.add_argument( "--emit", default="bytecode", help="MicroPython emitter to use (bytecode or native)" @@ -264,6 +277,12 @@ def main(): cmd_parser.add_argument("--heapsize", help="heapsize to use (use default if not specified)") cmd_parser.add_argument("--via-mpy", action="store_true", help="compile code to .mpy first") cmd_parser.add_argument("--mpy-cross-flags", default="", help="flags to pass to mpy-cross") + cmd_parser.add_argument( + "-r", + "--result-dir", + default=base_path("results"), + help="directory for test results", + ) cmd_parser.add_argument( "N", nargs=1, help="N parameter (approximate target CPU frequency in MHz)" ) @@ -282,15 +301,16 @@ def main(): M = int(args.M[0]) n_average = int(args.average) - if args.pyboard: - if not args.mpy_cross_flags: - args.mpy_cross_flags = "-march=armv7m" - target = pyboard.Pyboard(args.device) - target.enter_raw_repl() - else: + target = get_test_instance(args.test_instance, args.baudrate, args.user, args.password) + if target is None: + # Use the unix port of MicroPython. target = [MICROPYTHON, "-X", "emit=" + args.emit] if args.heapsize is not None: target.extend(["-X", "heapsize=" + args.heapsize]) + else: + # Use a remote target. + if not args.mpy_cross_flags: + args.mpy_cross_flags = "-march=armv7m" if len(args.files) == 0: tests_skip = ("benchrun.py",) @@ -307,13 +327,15 @@ def main(): print("N={} M={} n_average={}".format(N, M, n_average)) - target_had_error = run_benchmarks(args, target, N, M, n_average, tests) + os.makedirs(args.result_dir, exist_ok=True) + test_results = run_benchmarks(args, target, N, M, n_average, tests) + res = create_test_report(args, test_results) - if isinstance(target, pyboard.Pyboard): + if hasattr(target, "exit_raw_repl"): target.exit_raw_repl() target.close() - if target_had_error: + if not res: sys.exit(1) diff --git a/tests/run-tests.py b/tests/run-tests.py index cd1d681af1e..344adc8faa6 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -6,7 +6,6 @@ import sysconfig import platform import argparse -import inspect import json import re from glob import glob @@ -15,17 +14,30 @@ import threading import tempfile -# Maximum time to run a PC-based test, in seconds. -TEST_TIMEOUT = 30 - -# See stackoverflow.com/questions/2632199: __file__ nor sys.argv[0] -# are guaranteed to always work, this one should though. -BASEPATH = os.path.dirname(os.path.abspath(inspect.getsourcefile(lambda: None))) - - -def base_path(*p): - return os.path.abspath(os.path.join(BASEPATH, *p)).replace("\\", "/") - +from test_utils import ( + base_path, + pyboard, + TEST_TIMEOUT, + TEST_MAXIMUM_RAW_REPL_FAILURES, + MPYCROSS, + test_instance_description, + test_instance_epilog, + test_directory_description, + rm_f, + normalize_newlines, + set_injected_prologue, + get_results_filename, + convert_device_shortcut_to_real_device, + get_test_instance, + prepare_script_for_target, + create_test_report, + FLAKY_REASON_PREFIX, +) + +RV32_ARCH_FLAGS = { + "zba": 1 << 0, + "zcmp": 1 << 1, +} # Tests require at least CPython 3.3. If your default python3 executable # is of lower version, you can point MICROPY_CPYTHON3 environment var @@ -35,64 +47,276 @@ def base_path(*p): MICROPYTHON = os.getenv( "MICROPY_MICROPYTHON", base_path("../ports/windows/build-standard/micropython.exe") ) - # mpy-cross is only needed if --via-mpy command-line arg is passed - MPYCROSS = os.getenv("MICROPY_MPYCROSS", base_path("../mpy-cross/build/mpy-cross.exe")) else: CPYTHON3 = os.getenv("MICROPY_CPYTHON3", "python3") MICROPYTHON = os.getenv( "MICROPY_MICROPYTHON", base_path("../ports/unix/build-standard/micropython") ) - # mpy-cross is only needed if --via-mpy command-line arg is passed - MPYCROSS = os.getenv("MICROPY_MPYCROSS", base_path("../mpy-cross/build/mpy-cross")) # Use CPython options to not save .pyc files, to only access the core standard library # (not site packages which may clash with u-module names), and improve start up time. CPYTHON3_CMD = [CPYTHON3, "-BS"] -# File with the test results. -RESULTS_FILE = "_results.json" - # For diff'ing test output DIFF = os.getenv("MICROPY_DIFF", "diff -u") # Set PYTHONIOENCODING so that CPython will use utf-8 on systems which set another encoding in the locale os.environ["PYTHONIOENCODING"] = "utf-8" -# Code to allow a target MicroPython to import an .mpy from RAM -injected_import_hook_code = """\ -import sys, os, io, vfs -class __File(io.IOBase): - def __init__(self): - self.off = 0 - def ioctl(self, request, arg): - return 0 - def readinto(self, buf): - buf[:] = memoryview(__buf)[self.off:self.off + len(buf)] - self.off += len(buf) - return len(buf) -class __FS: - def mount(self, readonly, mkfs): - pass - def umount(self): - pass - def chdir(self, path): - pass - def stat(self, path): - if path == '__injected_test.mpy': - return tuple(0 for _ in range(10)) - else: - raise OSError(-2) # ENOENT - def open(self, path, mode): - return __File() -vfs.mount(__FS(), '/__vfstest') -os.chdir('/__vfstest') -__import__('__injected_test') -""" - - -def rm_f(fname): - if os.path.exists(fname): - os.remove(fname) +# Platforms associated with the unix port, values of `sys.platform`. +PC_PLATFORMS = ("darwin", "linux", "win32") + +# Mapping from `sys.platform` to the port name, for special cases. +# See `platform_to_port()` function. +platform_to_port_map = {"pyboard": "stm32", "WiPy": "cc3200"} +platform_to_port_map.update({p: "unix" for p in PC_PLATFORMS}) + +# Tests to skip for values of the `--via-mpy` argument. +via_mpy_tests_to_skip = { + # Skip the following when mpy is enabled. + True: ( + # These print out the filename and that's expected to match the .py name. + "import/import_file.py", + "io/argv.py", + "misc/sys_settrace_features.py", + "misc/sys_settrace_generator.py", + "misc/sys_settrace_loop.py", + ), +} + +# Tests to skip for specific emitters. +emitter_tests_to_skip = { + # Some tests are known to fail with native emitter. + # Remove them from the below when they work. + "native": ( + # These require raise_varargs. + "basics/gen_yield_from_close.py", + "basics/try_finally_return2.py", + "basics/try_reraise.py", + "basics/try_reraise2.py", + "misc/features.py", + # These require checking for unbound local. + "basics/annotate_var.py", + "basics/del_deref.py", + "basics/del_local.py", + "basics/scope_implicit.py", + "basics/unboundlocal.py", + # These require "raise from". + "basics/exception_chain.py", + # These require stack-allocated slice optimisation. + "micropython/heapalloc_slice.py", + # These require implicitly running the scheduler between bytecodes. + "micropython/schedule.py", + # These require sys.exc_info(). + "misc/sys_exc_info.py", + # These require sys.settrace(). + "misc/sys_settrace_cov.py", + "misc/sys_settrace_features.py", + "misc/sys_settrace_generator.py", + "misc/sys_settrace_loop.py", + # These are bytecode-specific tests. + "stress/bytecode_limit.py", + ), +} + +# Tests to skip on specific targets. +# These are tests that are difficult to detect that they should not be run on the given target. +platform_tests_to_skip = { + "esp8266": ( + "stress/list_sort.py", # watchdog kicks in because it takes too long + ), + "minimal": ( + "basics/class_inplace_op.py", # all special methods not supported + "basics/subclass_native_init.py", # native subclassing corner cases not support + "micropython/opt_level.py", # don't assume line numbers are stored + ), + "nrf": ( + "basics/io_buffered_writer.py", + "basics/io_bytesio_cow.py", + "basics/io_bytesio_ext.py", + "basics/io_bytesio_ext2.py", + "basics/io_iobase.py", + "basics/io_stringio1.py", + "basics/io_stringio_base.py", + "basics/io_stringio_with.py", + "basics/io_write_ext.py", + "basics/memoryview1.py", # no item assignment for memoryview + "extmod/random_basic.py", # unimplemented: random.seed + "micropython/opt_level.py", # no support for line numbers + "misc/non_compliant.py", # no item assignment for bytearray + ), + "renesas-ra": ( + "extmod/time_time_ns.py", # RA fsp rtc function doesn't support nano sec info + ), + "rp2": ( + # Skip thread tests that require more that 2 threads. + "thread/stress_heap.py", + "thread/thread_lock2.py", + "thread/thread_lock3.py", + "thread/thread_shared2.py", + ), + "webassembly": ( + "basics/string_format_modulo.py", # can't print nulls to stdout + "basics/string_strip.py", # can't print nulls to stdout + "basics/weakref_callback_exception.py", # has different exception printing output + "basics/weakref_ref_collect.py", # requires custom test due to GC behaviour + "basics/weakref_finalize_collect.py", # requires custom test due to GC behaviour + "extmod/asyncio_basic2.py", + "extmod/asyncio_cancel_self.py", + "extmod/asyncio_current_task.py", + "extmod/asyncio_exception.py", + "extmod/asyncio_gather_finished_early.py", + "extmod/asyncio_get_event_loop.py", + "extmod/asyncio_heaplock.py", + "extmod/asyncio_loop_stop.py", + "extmod/asyncio_new_event_loop.py", + "extmod/asyncio_threadsafeflag.py", + "extmod/asyncio_wait_for_fwd.py", + "extmod/asyncio_event_queue.py", + "extmod/asyncio_iterator_event.py", + "extmod/asyncio_wait_for_linked_task.py", + "extmod/binascii_a2b_base64.py", + "extmod/deflate_compress_memory_error.py", # tries to allocate unlimited memory + "extmod/re_stack_overflow.py", + "extmod/re_stack_overflow2.py", + "extmod/time_res.py", + "extmod/vfs_posix.py", + "extmod/vfs_posix_enoent.py", + "extmod/vfs_posix_paths.py", + "extmod/vfs_userfs.py", + "micropython/emg_exc.py", + "micropython/extreme_exc.py", + "micropython/heapalloc_exc_compressed_emg_exc.py", + ), + "WiPy": ( + "misc/print_exception.py", # requires error reporting full + ), + "zephyr": ( + # Skip thread tests that require more than 4 threads. + "thread/stress_heap.py", + "thread/thread_lock3.py", + ), +} + +# Tests to skip when MICROPY_ERROR_REPORTING is at a certain level. +error_reporting_tests_to_skip = { + # Skip at level MICROPY_ERROR_REPORTING_NONE. + "none": ( + "micropython/heapalloc_exc_compressed.py", + "micropython/heapalloc_exc_compressed_emg_exc.py", + "micropython/opt_level_lineno.py", + "misc/print_exception.py", + ), +} +# Skip at level MICROPY_ERROR_REPORTING_TERSE. +error_reporting_tests_to_skip["terse"] = error_reporting_tests_to_skip["none"] + +# Tests with known intermittent failures. These tests still run, but failures +# are reclassified as "ignored" instead of "fail" so they don't affect the CI +# exit code. Paths are relative to the tests/ directory (must match test_file +# format used by run_one_test, which normalises backslashes to forward slashes). +# +# Values are (reason, platforms) tuples where platforms is None (all platforms) +# or a tuple of sys.platform strings to restrict ignoring to those platforms. +flaky_tests_to_ignore = { + "thread/thread_gc1.py": ("GC race condition", None), + "thread/stress_schedule.py": ("intermittent crash under QEMU", None), + "thread/stress_recurse.py": ("stack overflow under emulation", None), + "thread/stress_heap.py": ("flaky on macOS", ("darwin",)), + "cmdline/repl_lock.py": ("REPL timing under QEMU", None), + "cmdline/repl_cont.py": ("REPL escaping on macOS", ("darwin",)), + "extmod/time_time_ns.py": ("CI runner clock precision", None), +} + +# These tests don't test float explicitly but rather use it to perform the test. +tests_requiring_float = ( + "extmod/asyncio_basic.py", + "extmod/asyncio_basic2.py", + "extmod/asyncio_cancel_task.py", + "extmod/asyncio_event.py", + "extmod/asyncio_fair.py", + "extmod/asyncio_gather.py", + "extmod/asyncio_gather_notimpl.py", + "extmod/asyncio_get_event_loop.py", + "extmod/asyncio_iterator_event.py", + "extmod/asyncio_lock.py", + "extmod/asyncio_task_done.py", + "extmod/asyncio_wait_for.py", + "extmod/asyncio_wait_for_fwd.py", + "extmod/asyncio_wait_for_linked_task.py", + "extmod/asyncio_wait_task.py", + "extmod/json_dumps_float.py", + "extmod/json_loads_float.py", + "extmod/random_extra_float.py", + "extmod/select_poll_eintr.py", + "extmod/tls_threads.py", + "extmod/uctypes_le_float.py", + "extmod/uctypes_native_float.py", + "extmod/uctypes_sizeof_float.py", + "extmod/vfs_rom.py", + "micropython/const_float.py", + "misc/rge_sm.py", + "ports/unix/ffi_float.py", + "ports/unix/ffi_float2.py", +) + +# These tests don't test slice explicitly but rather use it to perform the test. +tests_requiring_slice = ( + "basics/builtin_range.py", + "basics/bytearray1.py", + "basics/class_super.py", + "basics/containment.py", + "basics/errno1.py", + "basics/fun_str.py", + "basics/generator1.py", + "basics/globals_del.py", + "basics/memoryview1.py", + "basics/memoryview_gc.py", + "basics/object1.py", + "basics/python34.py", + "basics/struct_endian.py", + "extmod/btree1.py", + "extmod/deflate_decompress.py", + "extmod/framebuf16.py", + "extmod/framebuf4.py", + "extmod/machine1.py", + "extmod/time_mktime.py", + "extmod/time_res.py", + "extmod/tls_sslcontext_ciphers.py", + "extmod/vfs_blockdev_invalid2.py", + "extmod/vfs_fat_fileio1.py", + "extmod/vfs_fat_finaliser.py", + "extmod/vfs_fat_more.py", + "extmod/vfs_fat_ramdisk.py", + "extmod/vfs_fat_ramdisklarge.py", + "extmod/vfs_lfs.py", + "extmod/vfs_rom.py", + "float/string_format_modulo.py", + "micropython/builtin_execfile.py", + "micropython/extreme_exc.py", + "micropython/heapalloc_fail_bytearray.py", + "micropython/heapalloc_fail_list.py", + "micropython/heapalloc_fail_memoryview.py", + "micropython/import_mpy_invalid.py", + "micropython/import_mpy_native.py", + "micropython/import_mpy_native_gc.py", + "micropython/ringio_big.py", + "misc/non_compliant.py", + "misc/rge_sm.py", +) + +# Tests that require `import target_wiring` to work. +tests_requiring_target_wiring = ( + "extmod/machine_spi_rate.py", + "extmod/machine_uart_irq_txidle.py", + "extmod/machine_uart_tx.py", + "extmod_hardware/machine_can_timings.py", + "extmod_hardware/machine_encoder.py", + "extmod_hardware/machine_pwm.py", + "extmod_hardware/machine_uart_irq_break.py", + "extmod_hardware/machine_uart_irq_rx.py", + "extmod_hardware/machine_uart_irq_rxidle.py", +) # unescape wanted regex chars and escape unwanted ones @@ -115,74 +339,126 @@ def convert_regex_escapes(line): return bytes("".join(cs), "utf8") -def prepare_script_for_target(args, *, script_filename=None, script_text=None, force_plain=False): - if force_plain or (not args.via_mpy and args.emit == "bytecode"): - if script_filename is not None: - with open(script_filename, "rb") as f: - script_text = f.read() - elif args.via_mpy: - tempname = tempfile.mktemp(dir="") - mpy_filename = tempname + ".mpy" - - if script_filename is None: - script_filename = tempname + ".py" - cleanup_script_filename = True - with open(script_filename, "wb") as f: - f.write(script_text) - else: - cleanup_script_filename = False - - try: - subprocess.check_output( - [MPYCROSS] - + args.mpy_cross_flags.split() - + ["-o", mpy_filename, "-X", "emit=" + args.emit, script_filename], - stderr=subprocess.STDOUT, - ) - except subprocess.CalledProcessError as er: - return True, b"mpy-cross crash\n" + er.output +def platform_to_port(platform): + return platform_to_port_map.get(platform, platform) - with open(mpy_filename, "rb") as f: - script_text = b"__buf=" + bytes(repr(f.read()), "ascii") + b"\n" - rm_f(mpy_filename) - if cleanup_script_filename: - rm_f(script_filename) +def detect_inline_asm_arch(pyb, args): + for arch in ("rv32", "thumb", "xtensa"): + output = run_feature_check(pyb, args, "inlineasm_{}.py".format(arch)) + if output.strip() == arch.encode(): + return arch + return None - script_text += bytes(injected_import_hook_code, "ascii") - else: - print("error: using emit={} must go via .mpy".format(args.emit)) - sys.exit(1) - return False, script_text +def map_rv32_arch_flags(flags): + mapped_flags = [] + for extension, bit in RV32_ARCH_FLAGS.items(): + if flags & bit: + mapped_flags.append(extension) + flags &= ~bit + if flags: + raise Exception("Unexpected flag bits set in value {}".format(flags)) + return mapped_flags -def run_script_on_remote_target(pyb, args, test_file, is_special): - had_crash, script = prepare_script_for_target( - args, script_filename=test_file, force_plain=is_special +def detect_test_platform(pyb, args): + # Run a script to detect various bits of information about the target test instance. + output = run_feature_check(pyb, args, "target_info.py") + if output.endswith(b"CRASH"): + raise ValueError("cannot detect platform: {}".format(output)) + platform, arch, arch_flags, build, thread, float_prec, unicode, error_reporting = ( + str(output, "ascii").strip().split() ) - if had_crash: - return True, script - - try: - had_crash = False - pyb.enter_raw_repl() - output_mupy = pyb.exec_(script) - except pyboard.PyboardError as e: - had_crash = True - if not is_special and e.args[0] == "exception": - output_mupy = e.args[1] + e.args[2] + b"CRASH" - else: - output_mupy = bytes(e.args[0], "ascii") + b"\nCRASH" - return had_crash, output_mupy - - -special_tests = [ + if arch == "None": + arch = None + inlineasm_arch = detect_inline_asm_arch(pyb, args) + if thread == "None": + thread = None + float_prec = int(float_prec) + unicode = unicode == "True" + if arch == "rv32imc": + arch_flags = map_rv32_arch_flags(int(arch_flags)) + else: + arch_flags = None + + args.platform = platform + args.arch = arch + args.arch_flags = arch_flags + if arch and not args.mpy_cross_flags: + args.mpy_cross_flags = "-march=" + arch + if arch_flags: + args.mpy_cross_flags += " -march-flags=" + ",".join(arch_flags) + args.inlineasm_arch = inlineasm_arch + args.build = build + args.thread = thread + args.float_prec = float_prec + args.unicode = unicode + args.error_reporting = error_reporting + + # Print the detected information about the target. + print("platform={}".format(platform), end="") + if arch: + print(" arch={}".format(arch), end="") + if arch_flags: + print(" arch_flags={}".format(",".join(arch_flags)), end="") + if inlineasm_arch: + print(" inlineasm={}".format(inlineasm_arch), end="") + if thread: + print(" thread={}".format(thread), end="") + if float_prec: + print(" float={}-bit".format(float_prec), end="") + if unicode: + print(" unicode", end="") + + +def detect_target_wiring_script(pyb, args): + tw_data = b"" + tw_source = None + if args.target_wiring: + # A target_wiring path is explicitly provided, so use that. + tw_source = args.target_wiring + with open(tw_source, "rb") as f: + tw_data = f.read() + elif hasattr(pyb, "exec_raw") and pyb.exec_raw("import target_wiring") == (b"", b""): + # The board already has a target_wiring module available, so use that. + tw_source = "on-device" + else: + port = platform_to_port(args.platform) + build = args.build + tw_board_exact = None + tw_board_partial = None + tw_port = None + for file in os.listdir("target_wiring"): + file_base = file.removesuffix(".py") + if file_base == build: + # A file matching the target's board/build name. + tw_board_exact = file + elif file_base.endswith("x") and build.startswith(file_base.removesuffix("x")): + # A file with a partial match to the target's board/build name. + tw_board_partial = file + elif file_base == port: + # A file matching the target's port. + tw_port = file + tw_source = tw_board_exact or tw_board_partial or tw_port + if tw_source: + with open("target_wiring/" + tw_source, "rb") as f: + tw_data = f.read() + if tw_source: + print(" target_wiring={}".format(tw_source), end="") + pyb.target_wiring_script = tw_data + + +tests_with_regex_output = [ base_path(file) for file in ( "micropython/meminfo.py", "basics/bytes_compare3.py", "basics/builtin_help.py", + "basics/weakref_callback_exception.py", + "misc/sys_settrace_cov.py", + "net_inet/tls_text_errors.py", + "ports/unix/extra_coverage.py", "thread/thread_exc2.py", "ports/esp32/partition_ota.py", ) @@ -193,21 +469,23 @@ def run_micropython(pyb, args, test_file, test_file_abspath, is_special=False): had_crash = False if pyb is None: # run on PC - if ( - test_file_abspath.startswith((base_path("cmdline/"), base_path("feature_check/"))) - or test_file_abspath in special_tests - ): + if test_file_abspath.startswith((base_path("cmdline/"), base_path("feature_check/"))): # special handling for tests of the unix cmdline program is_special = True if is_special: # check for any cmdline options needed for this test - args = [MICROPYTHON] + cmdlist = [MICROPYTHON] + send_sigint = False with open(test_file, "rb") as f: - line = f.readline() - if line.startswith(b"# cmdline:"): - # subprocess.check_output on Windows only accepts strings, not bytes - args += [str(c, "utf-8") for c in line[10:].strip().split()] + for line in f: + if line.startswith(b"# cmdline:"): + # subprocess.check_output on Windows only accepts strings, not bytes + cmdlist += [str(c, "utf-8") for c in line[10:].strip().split()] + elif line.startswith(b"# sigint:"): + send_sigint = True + elif not line.startswith(b"#"): + break # run the test, possibly with redirected input try: @@ -235,33 +513,86 @@ def get(required=False): return rv def send_get(what): + # Detect hex {\x00} pattern and convert to ctrl-key codes. + ctrl_code = lambda m: bytes([int(m.group(1), 16)]) + what = re.sub(rb"{\\x(\d\d)}", ctrl_code, what) + os.write(master, what) return get() + def send_ctrl_c(): + # Send \x03 without trailing newline and wait for + # the full response (traceback + new prompt). + os.write(master, b"\x03") + return get(True) + with open(test_file, "rb") as f: - # instead of: output_mupy = subprocess.check_output(args, stdin=f) + # instead of: output_mupy = subprocess.check_output(cmdlist, stdin=f) master, slave = pty.openpty() - p = subprocess.Popen( - args, stdin=slave, stdout=slave, stderr=subprocess.STDOUT, bufsize=0 - ) - banner = get(True) - output_mupy = banner + b"".join(send_get(line) for line in f) - send_get(b"\x04") # exit the REPL, so coverage info is saved - # At this point the process might have exited already, but trying to - # kill it 'again' normally doesn't result in exceptions as Python and/or - # the OS seem to try to handle this nicely. When running Linux on WSL - # though, the situation differs and calling Popen.kill after the process - # terminated results in a ProcessLookupError. Just catch that one here - # since we just want the process to be gone and that's the case. try: - p.kill() - except ProcessLookupError: - pass - os.close(master) - os.close(slave) + preexec_fn = None + use_sigint_kill = False + # Tests with "# sigint:" need Ctrl-C (\x03) to + # generate SIGINT. MicroPython restores original + # terminal mode (ISIG on) during code execution, + # so on Linux we set up the PTY as a controlling + # terminal for proper signal delivery. On macOS, + # setsid/TIOCSCTTY breaks PTY I/O, so we fall + # back to os.kill(). + if send_sigint: + if sys.platform == "darwin": + use_sigint_kill = True + else: + import fcntl + import termios + + def preexec_fn(): + os.setsid() + fcntl.ioctl(0, termios.TIOCSCTTY, 0) + os.tcsetpgrp(0, os.getpid()) + + p = subprocess.Popen( + cmdlist, + stdin=slave, + stdout=slave, + stderr=subprocess.STDOUT, + bufsize=0, + preexec_fn=preexec_fn, + ) + banner = get(True) + if send_sigint: + import signal + + parts = [] + for line in f: + if b"{\\x03}" in line: + if use_sigint_kill: + os.kill(p.pid, signal.SIGINT) + parts.append(get(True)) + else: + parts.append(send_ctrl_c()) + else: + parts.append(send_get(line)) + output_mupy = banner + b"".join(parts) + else: + output_mupy = banner + b"".join(send_get(line) for line in f) + send_get(b"\x04") # exit the REPL, so coverage info is saved + # At this point the process might have exited already, but trying to + # kill it 'again' normally doesn't result in exceptions as Python and/or + # the OS seem to try to handle this nicely. When running Linux on WSL + # though, the situation differs and calling Popen.kill after the process + # terminated results in a ProcessLookupError. Just catch that one here + # since we just want the process to be gone and that's the case. + try: + p.kill() + except ProcessLookupError: + pass + finally: + os.close(master) + os.close(slave) else: output_mupy = subprocess.check_output( - args + [test_file], stderr=subprocess.STDOUT + cmdlist + [test_file], stderr=subprocess.STDOUT ) except subprocess.CalledProcessError: return b"CRASH" @@ -309,24 +640,30 @@ def send_get(what): else: # run via pyboard interface + requires_target_wiring = test_file.endswith(tests_requiring_target_wiring) had_crash, output_mupy = pyb.run_script_on_remote_target( - args, test_file_abspath, is_special + args, test_file_abspath, is_special, requires_target_wiring ) # canonical form for all ports/platforms is to use \n for end-of-line - output_mupy = output_mupy.replace(b"\r\n", b"\n") + output_mupy = normalize_newlines(output_mupy) # don't try to convert the output if we should skip this test - if had_crash or output_mupy in (b"SKIP\n", b"CRASH"): + if had_crash or output_mupy in (b"SKIP\n", b"SKIP-TOO-LARGE\n", b"CRASH"): return output_mupy # skipped special tests will output "SKIP" surrounded by other interpreter debug output if is_special and not had_crash and b"\nSKIP\n" in output_mupy: return b"SKIP\n" - if is_special or test_file_abspath in special_tests: + if is_special or test_file_abspath in tests_with_regex_output: # convert parts of the output that are not stable across runs - with open(test_file + ".exp", "rb") as f: + # Prefer emitter-specific expected output. + exp_file = test_file + "." + args.emit + ".exp" + if not os.path.isfile(exp_file): + # Fall back to generic expected output. + exp_file = test_file + ".exp" + with open(exp_file, "rb") as f: lines_exp = [] for line in f.readlines(): if line == b"########\n": @@ -376,6 +713,10 @@ def run_feature_check(pyb, args, test_file): return run_micropython(pyb, args, test_file_path, test_file_path, is_special=True) +class TestError(Exception): + pass + + class ThreadSafeCounter: def __init__(self, start=0): self._value = start @@ -396,75 +737,28 @@ def value(self): return self._value -class PyboardNodeRunner: - def __init__(self): - mjs = os.getenv("MICROPY_MICROPYTHON_MJS") - if mjs is None: - mjs = base_path("../ports/webassembly/build-standard/micropython.mjs") - else: - mjs = os.path.abspath(mjs) - self.micropython_mjs = mjs - - def close(self): - pass - - def run_script_on_remote_target(self, args, test_file, is_special): - cwd = os.path.dirname(test_file) - - # Create system command list. - cmdlist = ["node"] - if test_file.endswith(".py"): - # Run a Python script indirectly via "node micropython.mjs ". - cmdlist.append(self.micropython_mjs) - if args.heapsize is not None: - cmdlist.extend(["-X", "heapsize=" + args.heapsize]) - cmdlist.append(test_file) - else: - # Run a js/mjs script directly with Node, passing in the path to micropython.mjs. - cmdlist.append(test_file) - cmdlist.append(self.micropython_mjs) - - # Run the script. - try: - had_crash = False - output_mupy = subprocess.check_output( - cmdlist, stderr=subprocess.STDOUT, timeout=TEST_TIMEOUT, cwd=cwd - ) - except subprocess.CalledProcessError as er: - had_crash = True - output_mupy = er.output + b"CRASH" - except subprocess.TimeoutExpired as er: - had_crash = True - output_mupy = (er.output or b"") + b"TIMEOUT" - - # Return the results. - return had_crash, output_mupy - - def run_tests(pyb, tests, args, result_dir, num_threads=1): - test_count = ThreadSafeCounter() testcase_count = ThreadSafeCounter() - passed_count = ThreadSafeCounter() - failed_tests = ThreadSafeCounter([]) - skipped_tests = ThreadSafeCounter([]) + raw_repl_failure_count = ThreadSafeCounter() + test_results = ThreadSafeCounter([]) skip_tests = set() skip_native = False skip_int_big = False + skip_int_64 = False skip_bytearray = False skip_set_type = False skip_slice = False skip_async = False skip_const = False skip_revops = False - skip_io_module = False skip_fstring = False + skip_tstring = False skip_endian = False + skip_inlineasm = False has_complex = True has_coverage = False - upy_float_precision = 32 - if True: # Even if we run completely different tests in a different directory, # we need to access feature_checks from the same directory as the @@ -480,6 +774,11 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): if output != b"1000000000000000000000000000000000000000000000\n": skip_int_big = True + # Check if 'long long' precision integers are supported, even if arbitrary precision is not + output = run_feature_check(pyb, args, "int_64.py") + if output != b"4611686018427387904\n": + skip_int_64 = True + # Check if bytearray is supported, and skip such tests if it's not output = run_feature_check(pyb, args, "bytearray.py") if output != b"bytearray\n": @@ -510,30 +809,43 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): if output == b"TypeError\n": skip_revops = True - # Check if io module exists, and skip such tests if it doesn't - output = run_feature_check(pyb, args, "io_module.py") - if output != b"io\n": - skip_io_module = True - # Check if fstring feature is enabled, and skip such tests if it doesn't output = run_feature_check(pyb, args, "fstring.py") if output != b"a=1\n": skip_fstring = True - # Check if @micropython.asm_thumb supports Thumb2 instructions, and skip such tests if it doesn't - output = run_feature_check(pyb, args, "inlineasm_thumb2.py") - if output != b"thumb2\n": - skip_tests.add("inlineasm/asmbcc.py") - skip_tests.add("inlineasm/asmbitops.py") - skip_tests.add("inlineasm/asmconst.py") - skip_tests.add("inlineasm/asmdiv.py") - skip_tests.add("inlineasm/asmfpaddsub.py") - skip_tests.add("inlineasm/asmfpcmp.py") - skip_tests.add("inlineasm/asmfpldrstr.py") - skip_tests.add("inlineasm/asmfpmuldiv.py") - skip_tests.add("inlineasm/asmfpsqrt.py") - skip_tests.add("inlineasm/asmit.py") - skip_tests.add("inlineasm/asmspecialregs.py") + # Check if tstring feature is enabled, and skip such tests if it doesn't + output = run_feature_check(pyb, args, "tstring.py") + if output != b"tstring\n": + skip_tstring = True + + if args.inlineasm_arch == "thumb": + # Check if @micropython.asm_thumb supports Thumb2 instructions, and skip such tests if it doesn't + output = run_feature_check(pyb, args, "inlineasm_thumb2.py") + if output != b"thumb2\n": + skip_tests.add("inlineasm/thumb/asmbcc.py") + skip_tests.add("inlineasm/thumb/asmbitops.py") + skip_tests.add("inlineasm/thumb/asmconst.py") + skip_tests.add("inlineasm/thumb/asmdiv.py") + skip_tests.add("inlineasm/thumb/asmit.py") + skip_tests.add("inlineasm/thumb/asmspecialregs.py") + if args.arch not in ("armv7emsp", "armv7emdp"): + skip_tests.add("inlineasm/thumb/asmfpaddsub.py") + skip_tests.add("inlineasm/thumb/asmfpcmp.py") + skip_tests.add("inlineasm/thumb/asmfpldrstr.py") + skip_tests.add("inlineasm/thumb/asmfpmuldiv.py") + skip_tests.add("inlineasm/thumb/asmfpsqrt.py") + + if args.inlineasm_arch == "rv32": + # Discover extension-specific inlineasm tests and add them to the + # list of tests to run if applicable. + for extension in RV32_ARCH_FLAGS: + try: + output = run_feature_check(pyb, args, "inlineasm_rv32_{}.py".format(extension)) + if output.strip() != "rv32_{}".format(extension).encode(): + skip_tests.add("inlineasm/rv32/asm_ext_{}.py".format(extension)) + except FileNotFoundError: + pass # Check if emacs repl is supported, and skip such tests if it's not t = run_feature_check(pyb, args, "repl_emacs_check.py") @@ -546,11 +858,6 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): skip_tests.add("cmdline/repl_words_move.py") upy_byteorder = run_feature_check(pyb, args, "byteorder.py") - upy_float_precision = run_feature_check(pyb, args, "float.py") - try: - upy_float_precision = int(upy_float_precision) - except ValueError: - upy_float_precision = 0 has_complex = run_feature_check(pyb, args, "complex.py") == b"complex\n" has_coverage = run_feature_check(pyb, args, "coverage.py") == b"coverage\n" cpy_byteorder = subprocess.check_output( @@ -558,59 +865,43 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): ) skip_endian = upy_byteorder != cpy_byteorder - # These tests don't test slice explicitly but rather use it to perform the test - misc_slice_tests = ( - "builtin_range", - "bytearray1", - "class_super", - "containment", - "errno1", - "fun_str", - "generator1", - "globals_del", - "memoryview1", - "memoryview_gc", - "object1", - "python34", - "string_format_modulo", - "struct_endian", - ) + skip_inlineasm = args.inlineasm_arch is None # Some tests shouldn't be run on GitHub Actions if os.getenv("GITHUB_ACTIONS") == "true": - skip_tests.add("thread/stress_schedule.py") # has reliability issues - if os.getenv("RUNNER_OS") == "Windows" and os.getenv("CI_BUILD_CONFIGURATION") == "Debug": # fails with stack overflow on Debug builds skip_tests.add("misc/sys_settrace_features.py") - if upy_float_precision == 0: - skip_tests.add("extmod/uctypes_le_float.py") - skip_tests.add("extmod/uctypes_native_float.py") - skip_tests.add("extmod/uctypes_sizeof_float.py") - skip_tests.add("extmod/json_dumps_float.py") - skip_tests.add("extmod/json_loads_float.py") - skip_tests.add("extmod/random_extra_float.py") - skip_tests.add("misc/rge_sm.py") - if upy_float_precision < 32: + if args.float_prec == 0: + skip_tests.update(tests_requiring_float) + if args.float_prec < 32: skip_tests.add( "float/float2int_intbig.py" ) # requires fp32, there's float2int_fp30_intbig.py instead skip_tests.add( - "float/string_format.py" - ) # requires fp32, there's string_format_fp30.py instead + "float/float_struct_e.py" + ) # requires fp32, there's float_struct_e_fp30.py instead skip_tests.add("float/bytes_construct.py") # requires fp32 skip_tests.add("float/bytearray_construct.py") # requires fp32 skip_tests.add("float/float_format_ints_power10.py") # requires fp32 - if upy_float_precision < 64: + if args.float_prec < 64: skip_tests.add("float/float_divmod.py") # tested by float/float_divmod_relaxed.py instead skip_tests.add("float/float2int_doubleprec_intbig.py") + skip_tests.add("float/float_struct_e_doubleprec.py") skip_tests.add("float/float_format_ints_doubleprec.py") skip_tests.add("float/float_parse_doubleprec.py") + if not args.unicode: + skip_tests.add("extmod/json_loads.py") # tests loading a utf-8 character + + if skip_slice: + skip_tests.update(tests_requiring_slice) + if not has_complex: skip_tests.add("float/complex1.py") skip_tests.add("float/complex1_intbig.py") + skip_tests.add("float/complex1_micropython.py") skip_tests.add("float/complex_reverse_op.py") skip_tests.add("float/complex_special_methods.py") skip_tests.add("float/int_big_float.py") @@ -623,91 +914,31 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): skip_tests.add("cmdline/repl_sys_ps1_ps2.py") skip_tests.add("extmod/ssl_poll.py") - # Skip thread mutation tests on targets that don't have the GIL. - if args.target in ("rp2", "unix"): + # Skip thread mutation tests on targets that have unsafe threading behaviour. + if args.thread == "unsafe": for t in tests: if t.startswith("thread/mutate_"): skip_tests.add(t) - # Skip thread tests that require many threads on targets that don't support multiple threads. - if args.target == "rp2": - skip_tests.add("thread/stress_heap.py") - skip_tests.add("thread/thread_lock2.py") - skip_tests.add("thread/thread_lock3.py") - skip_tests.add("thread/thread_shared2.py") - elif args.target == "zephyr": - skip_tests.add("thread/stress_heap.py") - skip_tests.add("thread/thread_lock3.py") - # Some tests shouldn't be run on pyboard - if args.target != "unix": + if args.platform not in PC_PLATFORMS: skip_tests.add("basics/exception_chain.py") # warning is not printed skip_tests.add("micropython/meminfo.py") # output is very different to PC output + skip_tests.add("unicode/file1.py") # requires local file access + skip_tests.add("unicode/file2.py") # requires local file access + skip_tests.add("unicode/file_invalid.py") # requires local file access + + # Skip certain tests when going via a .mpy file. + skip_tests.update(via_mpy_tests_to_skip.get(args.via_mpy, ())) + + # Skip emitter-specific tests. + skip_tests.update(emitter_tests_to_skip.get(args.emit, ())) + + # Skip platform-specific tests. + skip_tests.update(platform_tests_to_skip.get(args.platform, ())) - if args.target == "wipy": - skip_tests.add("misc/print_exception.py") # requires error reporting full - skip_tests.update( - { - "extmod/uctypes_%s.py" % t - for t in "bytearray le native_le ptr_le ptr_native_le sizeof sizeof_native array_assign_le array_assign_native_le".split() - } - ) # requires uctypes - skip_tests.add("extmod/heapq1.py") # heapq not supported by WiPy - skip_tests.add("extmod/random_basic.py") # requires random - skip_tests.add("extmod/random_extra.py") # requires random - elif args.target == "esp8266": - skip_tests.add("micropython/viper_args.py") # too large - skip_tests.add("micropython/viper_binop_arith.py") # too large - skip_tests.add("misc/rge_sm.py") # too large - elif args.target == "minimal": - skip_tests.add("basics/class_inplace_op.py") # all special methods not supported - skip_tests.add( - "basics/subclass_native_init.py" - ) # native subclassing corner cases not support - skip_tests.add("misc/rge_sm.py") # too large - skip_tests.add("micropython/opt_level.py") # don't assume line numbers are stored - elif args.target == "nrf": - skip_tests.add("basics/memoryview1.py") # no item assignment for memoryview - skip_tests.add("extmod/random_basic.py") # unimplemented: random.seed - skip_tests.add("micropython/opt_level.py") # no support for line numbers - skip_tests.add("misc/non_compliant.py") # no item assignment for bytearray - for t in tests: - if t.startswith("basics/io_"): - skip_tests.add(t) - elif args.target == "renesas-ra": - skip_tests.add( - "extmod/time_time_ns.py" - ) # RA fsp rtc function doesn't support nano sec info - elif args.target == "qemu": - skip_tests.add("inlineasm/asmfpaddsub.py") # requires Cortex-M4 - skip_tests.add("inlineasm/asmfpcmp.py") - skip_tests.add("inlineasm/asmfpldrstr.py") - skip_tests.add("inlineasm/asmfpmuldiv.py") - skip_tests.add("inlineasm/asmfpsqrt.py") - elif args.target == "webassembly": - skip_tests.add("basics/string_format_modulo.py") # can't print nulls to stdout - skip_tests.add("basics/string_strip.py") # can't print nulls to stdout - skip_tests.add("extmod/asyncio_basic2.py") - skip_tests.add("extmod/asyncio_cancel_self.py") - skip_tests.add("extmod/asyncio_current_task.py") - skip_tests.add("extmod/asyncio_exception.py") - skip_tests.add("extmod/asyncio_gather_finished_early.py") - skip_tests.add("extmod/asyncio_get_event_loop.py") - skip_tests.add("extmod/asyncio_heaplock.py") - skip_tests.add("extmod/asyncio_loop_stop.py") - skip_tests.add("extmod/asyncio_new_event_loop.py") - skip_tests.add("extmod/asyncio_threadsafeflag.py") - skip_tests.add("extmod/asyncio_wait_for_fwd.py") - skip_tests.add("extmod/binascii_a2b_base64.py") - skip_tests.add("extmod/re_stack_overflow.py") - skip_tests.add("extmod/time_res.py") - skip_tests.add("extmod/vfs_posix.py") - skip_tests.add("extmod/vfs_posix_enoent.py") - skip_tests.add("extmod/vfs_posix_paths.py") - skip_tests.add("extmod/vfs_userfs.py") - skip_tests.add("micropython/emg_exc.py") - skip_tests.add("micropython/extreme_exc.py") - skip_tests.add("micropython/heapalloc_exc_compressed_emg_exc.py") + # Skip error-reporting-specific tests. + skip_tests.update(error_reporting_tests_to_skip.get(args.error_reporting, ())) # Some tests are known to fail on 64-bit machines if pyb is None and platform.architecture()[0] == "64bit": @@ -719,47 +950,26 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): # Works but CPython uses '\' path separator skip_tests.add("import/import_file.py") - # Some tests are known to fail with native emitter - # Remove them from the below when they work - if args.emit == "native": - skip_tests.add("basics/gen_yield_from_close.py") # require raise_varargs - skip_tests.update( - {"basics/%s.py" % t for t in "try_reraise try_reraise2".split()} - ) # require raise_varargs - skip_tests.add("basics/annotate_var.py") # requires checking for unbound local - skip_tests.add("basics/del_deref.py") # requires checking for unbound local - skip_tests.add("basics/del_local.py") # requires checking for unbound local - skip_tests.add("basics/exception_chain.py") # raise from is not supported - skip_tests.add("basics/scope_implicit.py") # requires checking for unbound local - skip_tests.add("basics/sys_tracebacklimit.py") # requires traceback info - skip_tests.add("basics/try_finally_return2.py") # requires raise_varargs - skip_tests.add("basics/unboundlocal.py") # requires checking for unbound local - skip_tests.add("misc/features.py") # requires raise_varargs - skip_tests.add( - "misc/print_exception.py" - ) # because native doesn't have proper traceback info - skip_tests.add("misc/sys_exc_info.py") # sys.exc_info() is not supported for native - skip_tests.add("misc/sys_settrace_features.py") # sys.settrace() not supported - skip_tests.add("misc/sys_settrace_generator.py") # sys.settrace() not supported - skip_tests.add("misc/sys_settrace_loop.py") # sys.settrace() not supported - skip_tests.add( - "micropython/emg_exc.py" - ) # because native doesn't have proper traceback info - skip_tests.add( - "micropython/heapalloc_traceback.py" - ) # because native doesn't have proper traceback info - skip_tests.add( - "micropython/opt_level_lineno.py" - ) # native doesn't have proper traceback info - skip_tests.add("micropython/schedule.py") # native code doesn't check pending events - skip_tests.add("stress/bytecode_limit.py") # bytecode specific test + skip_tests = [os.path.realpath(base_path(skip_test)) for skip_test in skip_tests] def run_one_test(test_file): - test_file = test_file.replace("\\", "/") test_file_abspath = os.path.abspath(test_file).replace("\\", "/") + # If test_file is one of our own tests always make it relative to our tests/ dir and + # otherwise use the absolute path, regardless of actual path passed, + # such that display and result output is always the same. + try: + test_file_relpath = os.path.relpath(test_file, start=base_path()) + if not test_file_relpath.startswith(".."): + test_file = test_file_relpath + else: + test_file = test_file_abspath + except ValueError: + # Path on different drive on Windows. + test_file = test_file_abspath + test_file = test_file.replace("\\", "/") if args.filters: - # Default verdict is the opposit of the first action + # Default verdict is the opposite of the first action verdict = "include" if args.filters[0][0] == "exclude" else "exclude" for action, pat in args.filters: if pat.search(test_file): @@ -769,62 +979,51 @@ def run_one_test(test_file): test_basename = test_file.replace("..", "_").replace("./", "").replace("/", "_") test_name = os.path.splitext(os.path.basename(test_file))[0] - is_native = ( - test_name.startswith("native_") - or test_name.startswith("viper_") - or args.emit == "native" - ) + is_native = test_name.startswith("native_") or test_name.startswith("viper_") is_endian = test_name.endswith("_endian") - is_int_big = test_name.startswith("int_big") or test_name.endswith("_intbig") + is_int_big = ( + test_name.startswith("int_big") + or test_name.endswith("_intbig") + or test_name.startswith("ffi_int") # these tests contain large integer literals + ) + is_int_64 = test_name.startswith("int_64") or test_name.endswith("_int64") is_bytearray = test_name.startswith("bytearray") or test_name.endswith("_bytearray") is_set_type = test_name.startswith(("set_", "frozenset")) or test_name.endswith("_set") - is_slice = test_name.find("slice") != -1 or test_name in misc_slice_tests - is_async = test_name.startswith(("async_", "asyncio_")) + is_slice = test_name.find("slice") != -1 + is_async = test_name.startswith(("async_", "asyncio_")) or test_name.endswith("_async") is_const = test_name.startswith("const") - is_io_module = test_name.startswith("io_") is_fstring = test_name.startswith("string_fstring") + is_tstring = test_name.startswith("string_tstring") or test_name.endswith("_tstring") + is_inlineasm = test_name.startswith("asm") - skip_it = test_file in skip_tests + skip_it = os.path.realpath(test_file) in skip_tests skip_it |= skip_native and is_native skip_it |= skip_endian and is_endian skip_it |= skip_int_big and is_int_big + skip_it |= skip_int_64 and is_int_64 skip_it |= skip_bytearray and is_bytearray skip_it |= skip_set_type and is_set_type skip_it |= skip_slice and is_slice skip_it |= skip_async and is_async skip_it |= skip_const and is_const skip_it |= skip_revops and "reverse_op" in test_name - skip_it |= skip_io_module and is_io_module skip_it |= skip_fstring and is_fstring + skip_it |= skip_tstring and is_tstring + skip_it |= skip_inlineasm and is_inlineasm if skip_it: print("skip ", test_file) - skipped_tests.append(test_name) + test_results.append((test_file, "skip", "")) + return + elif args.dry_run: + print("found", test_file) + test_results.append((test_file, "found", "")) return - # get expected output - test_file_expected = test_file + ".exp" - if os.path.isfile(test_file_expected): - # expected output given by a file, so read that in - with open(test_file_expected, "rb") as f: - output_expected = f.read() - else: - # run CPython to work out expected output - try: - output_expected = subprocess.check_output( - CPYTHON3_CMD + [test_file_abspath], - cwd=os.path.dirname(test_file), - stderr=subprocess.STDOUT, - ) - except subprocess.CalledProcessError: - output_expected = b"CPYTHON3 CRASH" - - # canonical form for all host platforms is to use \n for end-of-line - output_expected = output_expected.replace(b"\r\n", b"\n") - - # run MicroPython + # Run the test on the MicroPython target. output_mupy = run_micropython(pyb, args, test_file, test_file_abspath) + # Check if the target requested to skip this test. if output_mupy == b"SKIP\n": if pyb is not None and hasattr(pyb, "read_until"): # Running on a target over a serial connection, and the target requested @@ -833,74 +1032,154 @@ def run_one_test(test_file): # start-up code (eg boot.py) when preparing to run the next test. pyb.read_until(1, b"raw REPL; CTRL-B to exit\r\n") print("skip ", test_file) - skipped_tests.append(test_name) + test_results.append((test_file, "skip", "")) return + elif output_mupy == b"SKIP-TOO-LARGE\n": + print("lrge ", test_file) + test_results.append((test_file, "skip", "too large")) + return + + # Look at the output of the test to see if unittest was used. + uses_unittest = False + output_mupy_lines = output_mupy.splitlines() + if any( + line == b"ImportError: no module named 'unittest'" for line in output_mupy_lines[-3:] + ): + raise TestError( + ( + "error: test {} requires unittest".format(test_file), + "(eg run `mpremote mip install unittest` to install it)", + ) + ) + elif ( + len(output_mupy_lines) > 4 + and output_mupy_lines[-4] == b"-" * 70 + and output_mupy_lines[-2] == b"" + ): + # look for unittest summary + unittest_ran_match = re.match(rb"Ran (\d+) tests$", output_mupy_lines[-3]) + unittest_result_match = re.match( + b"(" + rb"(OK)( \(skipped=(\d+)\))?" + b"|" + rb"(FAILED) \(failures=(\d+), errors=(\d+)\)" + b")$", + output_mupy_lines[-1], + ) + uses_unittest = unittest_ran_match and unittest_result_match - testcase_count.add(len(output_expected.splitlines())) + # Determine the expected output. + if uses_unittest: + # Expected output is result of running unittest. + output_expected = None + else: + # Prefer emitter-specific expected output. + test_file_expected = test_file + "." + args.emit + ".exp" + if not os.path.isfile(test_file_expected): + # Fall back to generic expected output. + test_file_expected = test_file + ".exp" + if os.path.isfile(test_file_expected): + # Expected output given by a file, so read that in. + with open(test_file_expected, "rb") as f: + output_expected = f.read() + else: + # Run CPython to work out expected output. + try: + output_expected = subprocess.check_output( + CPYTHON3_CMD + [test_file_abspath], + cwd=os.path.dirname(test_file), + stderr=subprocess.STDOUT, + ) + except subprocess.CalledProcessError as er: + output_expected = b"CPYTHON3 CRASH:\n" + er.output + + # Canonical form for all host platforms is to use \n for end-of-line. + output_expected = output_expected.replace(b"\r\n", b"\n") + + # Work out if test passed or not. + test_passed = False + extra_info = "" + if uses_unittest: + test_passed = unittest_result_match.group(2) == b"OK" + num_test_cases = int(unittest_ran_match.group(1)) + extra_info = "unittest: {} ran".format(num_test_cases) + if test_passed and unittest_result_match.group(4) is not None: + num_skipped = int(unittest_result_match.group(4)) + num_test_cases -= num_skipped + extra_info += ", {} skipped".format(num_skipped) + elif not test_passed: + num_failures = int(unittest_result_match.group(6)) + num_errors = int(unittest_result_match.group(7)) + extra_info += ", {} failures, {} errors".format(num_failures, num_errors) + extra_info = "(" + extra_info + ")" + testcase_count.add(num_test_cases) + else: + testcase_count.add(len(output_expected.splitlines())) + test_passed = output_expected == output_mupy filename_expected = os.path.join(result_dir, test_basename + ".exp") filename_mupy = os.path.join(result_dir, test_basename + ".out") - if output_expected == output_mupy: - print("pass ", test_file) - passed_count.increment() + # Print test summary, update counters, and save .exp/.out files if needed. + if test_passed: + print("pass ", test_file, extra_info) + test_results.append((test_file, "pass", "")) rm_f(filename_expected) rm_f(filename_mupy) else: - with open(filename_expected, "wb") as f: - f.write(output_expected) + if output_mupy.startswith(b"could not enter raw repl"): + extra_info = "raw REPL failed" + raw_repl_failure_count.increment() + print("FAIL ", test_file, extra_info) + if output_expected is not None: + with open(filename_expected, "wb") as f: + f.write(output_expected) + else: + rm_f(filename_expected) # in case left over from previous failed run with open(filename_mupy, "wb") as f: f.write(output_mupy) - print("FAIL ", test_file) - failed_tests.append((test_name, test_file)) - - test_count.increment() + test_results.append((test_file, "fail", "")) + + # Print a note if this looks like it might have been a misfired unittest + if not uses_unittest and not test_passed: + with open(test_file, "r") as f: + if any(re.match("^import.+unittest", l) for l in f.readlines()): + print( + "NOTE: {} may be a unittest that doesn't run unittest.main()".format( + test_file + ) + ) if pyb: num_threads = 1 - if num_threads > 1: - pool = ThreadPool(num_threads) - pool.map(run_one_test, tests) - else: - for test in tests: - run_one_test(test) - - print( - "{} tests performed ({} individual testcases)".format( - test_count.value, testcase_count.value - ) - ) - print("{} tests passed".format(passed_count.value)) - - skipped_tests = sorted(skipped_tests.value) - if len(skipped_tests) > 0: - print("{} tests skipped: {}".format(len(skipped_tests), " ".join(skipped_tests))) - failed_tests = sorted(failed_tests.value) - - # Serialize regex added by append_filter. - def to_json(obj): - if isinstance(obj, re.Pattern): - return obj.pattern - return obj - - with open(os.path.join(result_dir, RESULTS_FILE), "w") as f: - json.dump( - {"args": vars(args), "failed_tests": [test[1] for test in failed_tests]}, - f, - default=to_json, - ) + try: + if num_threads > 1: + pool = ThreadPool(num_threads) + pool.map(run_one_test, tests) + else: + for test in tests: + run_one_test(test) + if raw_repl_failure_count.value > TEST_MAXIMUM_RAW_REPL_FAILURES: + print("Too many raw REPL failures, aborting test run") + break + except TestError as er: + for line in er.args[0]: + print(line) + sys.exit(2) - if len(failed_tests) > 0: - print( - "{} tests failed: {}".format( - len(failed_tests), " ".join(test[0] for test in failed_tests) - ) - ) - return False + # Reclassify known-flaky test failures as ignored. + # Safe to mutate: thread pool has joined. + results = test_results.value + for i, r in enumerate(results): + if r[1] == "fail": + reason, platforms = flaky_tests_to_ignore.get(r[0], (None, None)) + if reason is not None: + if platforms is None or sys.platform in platforms: + results[i] = (r[0], "ignored", "{}: {}".format(FLAKY_REASON_PREFIX, reason)) - # all tests succeeded - return True + # Return test results. + return test_results.value, testcase_count.value class append_filter(argparse.Action): @@ -920,19 +1199,20 @@ def __call__(self, parser, args, value, option): def main(): cmd_parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, - description="""Run and manage tests for MicroPython. + description=f"""Run and manage tests for MicroPython. + +{test_instance_description} +{test_directory_description} -Tests are discovered by scanning test directories for .py files or using the -specified test files. If test files nor directories are specified, the script -expects to be ran in the tests directory (where this file is located) and the -builtin tests suitable for the target platform are ran. When running tests, run-tests.py compares the MicroPython output of the test with the output -produced by running the test through CPython unless a .exp file is found, in which -case it is used as comparison. +produced by running the test through CPython unless a .exp file is found (or a +.native.exp file when using the native emitter), in which case it is used as comparison. + If a test fails, run-tests.py produces a pair of .out and .exp files in the result directory with the MicroPython output and the expectations, respectively. """, - epilog="""\ + epilog=f"""\ +{test_instance_epilog} Options -i and -e can be multiple and processed in the order given. Regex "search" (vs "match") operation is used. An action (include/exclude) of the last matching regex is used: @@ -941,11 +1221,8 @@ def main(): run-tests.py -e async -i async_foo - include all, exclude async, yet still include async_foo """, ) - cmd_parser.add_argument("--target", default="unix", help="the target platform") cmd_parser.add_argument( - "--device", - default="/dev/ttyACM0", - help="the serial device or the IP address of the pyboard", + "-t", "--test-instance", default="unix", help="the MicroPython instance to test" ) cmd_parser.add_argument( "-b", "--baudrate", default=115200, help="the baud rate of the serial device" @@ -974,6 +1251,11 @@ def main(): dest="filters", help="include test by regex on path/name.py", ) + cmd_parser.add_argument( + "--dry-run", + action="store_true", + help="Show tests which would run (though might still be skipped at runtime)", + ) cmd_parser.add_argument( "--emit", default="bytecode", help="MicroPython emitter to use (bytecode or native)" ) @@ -1009,14 +1291,38 @@ def main(): action="store_true", help="re-run only the failed tests", ) + cmd_parser.add_argument( + "--begin", + metavar="PROLOGUE", + default=None, + help="prologue python file to execute before module import", + ) + cmd_parser.add_argument( + "--target-wiring", + default=None, + help="force the given script to be used as target_wiring.py", + ) args = cmd_parser.parse_args() + prologue = "" + if args.begin: + with open(args.begin, "rt") as source: + prologue = source.read() + set_injected_prologue(prologue) + if args.print_failures: - for exp in glob(os.path.join(args.result_dir, "*.exp")): - testbase = exp[:-4] + for out in glob(os.path.join(args.result_dir, "*.out")): + testbase = out[:-4] print() print("FAILURE {0}".format(testbase)) - os.system("{0} {1}.exp {1}.out".format(DIFF, testbase)) + if os.path.exists(testbase + ".exp"): + # Show diff of expected and actual output. + os.system("{0} {1}.exp {1}.out".format(DIFF, testbase)) + else: + # No expected output, just show the actual output (eg from a unittest). + with open(out) as f: + for line in f: + print(line, end="") sys.exit(0) @@ -1025,47 +1331,15 @@ def main(): os.path.join(args.result_dir, "*.out") ): os.remove(f) - rm_f(os.path.join(args.result_dir, RESULTS_FILE)) + rm_f(get_results_filename(args)) sys.exit(0) - LOCAL_TARGETS = ( - "unix", - "webassembly", - ) - EXTERNAL_TARGETS = ( - "pyboard", - "wipy", - "esp8266", - "esp32", - "minimal", - "nrf", - "qemu", - "renesas-ra", - "rp2", - "zephyr", - ) - if args.target in LOCAL_TARGETS: - pyb = None - if args.target == "webassembly": - pyb = PyboardNodeRunner() - elif args.target in EXTERNAL_TARGETS: - global pyboard - sys.path.append(base_path("../tools")) - import pyboard - - pyb = pyboard.Pyboard(args.device, args.baudrate, args.user, args.password) - pyboard.Pyboard.run_script_on_remote_target = run_script_on_remote_target - pyb.enter_raw_repl() - else: - raise ValueError("target must be one of %s" % ", ".join(LOCAL_TARGETS + EXTERNAL_TARGETS)) + # Get the test instance to run on. + pyb = get_test_instance(args.test_instance, args.baudrate, args.user, args.password) - # Automatically detect the native architecture for mpy-cross if not given. - if not args.mpy_cross_flags: - output = run_feature_check(pyb, args, "target_info.py") - arch = str(output, "ascii").strip() - if arch != "None": - args.mpy_cross_flags = "-march=" + arch + # Automatically detect the platform. + detect_test_platform(pyb, args) if args.run_failures and (any(args.files) or args.test_dirs is not None): raise ValueError( @@ -1073,66 +1347,65 @@ def main(): ) if args.run_failures: - results_file = os.path.join(args.result_dir, RESULTS_FILE) + results_file = get_results_filename(args) if os.path.exists(results_file): with open(results_file, "r") as f: - tests = json.load(f)["failed_tests"] + tests = list(test[0] for test in json.load(f)["results"] if test[1] == "fail") else: tests = [] elif len(args.files) == 0: test_extensions = ("*.py",) - if args.target == "webassembly": + if args.platform == "webassembly": test_extensions += ("*.js", "*.mjs") - if args.test_dirs is None: + all_test_dirs = [] + main_tests_dir_in_args = None + if args.test_dirs is not None: + # Run tests from given directories though if user explicitly passes this directory as argument + # still do the normal test discovery to be consistent with running from within this directory. + main_tests_dir = os.path.realpath(base_path()) + for test_dir in args.test_dirs: + if os.path.realpath(test_dir) == main_tests_dir: + main_tests_dir_in_args = test_dir + else: + all_test_dirs.append(test_dir) + + if args.test_dirs is None or main_tests_dir_in_args is not None: test_dirs = ( "basics", "micropython", "misc", "extmod", + "stress", ) - if args.target == "pyboard": - # run pyboard tests - test_dirs += ("float", "stress", "inlineasm", "ports/stm32") - elif args.target in ("renesas-ra"): - test_dirs += ("float", "inlineasm", "ports/renesas-ra") - elif args.target == "rp2": - test_dirs += ("float", "stress", "thread", "ports/rp2") - if "arm" in args.mpy_cross_flags: - test_dirs += ("inlineasm",) - elif args.target == "esp32": - test_dirs += ("float", "stress", "thread") - elif args.target in ("esp8266", "minimal", "nrf"): + if args.inlineasm_arch is not None: + test_dirs += ("inlineasm/{}".format(args.inlineasm_arch),) + if args.thread is not None: + test_dirs += ("thread",) + if args.float_prec > 0: test_dirs += ("float",) - elif args.target == "wipy": - # run WiPy tests - test_dirs += ("ports/cc3200",) - elif args.target == "unix": + if args.unicode: + test_dirs += ("unicode",) + port_specific_test_dir = "ports/{}".format(platform_to_port(args.platform)) + if os.path.isdir(port_specific_test_dir): + test_dirs += (port_specific_test_dir,) + if args.platform in PC_PLATFORMS: # run PC tests - test_dirs += ( - "float", - "import", - "io", - "stress", - "unicode", - "cmdline", - "ports/unix", - ) - elif args.target == "qemu": - test_dirs += ( - "float", - "inlineasm", - "ports/qemu", - ) - elif args.target == "webassembly": - test_dirs += ("float", "ports/webassembly") - else: - # run tests from these directories - test_dirs = args.test_dirs + test_dirs += ("import",) + if args.build != "minimal": + test_dirs += ("cmdline", "io") + + all_test_dirs.extend( + test_dir + if main_tests_dir_in_args is None + else os.path.join(main_tests_dir_in_args, test_dir) + for test_dir in test_dirs + ) + tests = sorted( test_file for test_files in ( - glob(os.path.join(dir, ext)) for dir in test_dirs for ext in test_extensions + glob(os.path.join(dir, ext)) for dir in all_test_dirs for ext in test_extensions ) for test_file in test_files ) @@ -1140,13 +1413,28 @@ def main(): # tests explicitly given tests = args.files + # If any tests need it, prepare the target_wiring script for the target. + if pyb and any(test.endswith(tests_requiring_target_wiring) for test in tests): + detect_target_wiring_script(pyb, args) + + # End the target information line. + print() + if not args.keep_path: - # clear search path to make sure tests use only builtin modules and those in extmod - os.environ["MICROPYPATH"] = ".frozen" + os.pathsep + base_path("../extmod") + # Clear search path to make sure tests use only builtin modules, those in + # extmod, and a path to unittest in case it's needed. + os.environ["MICROPYPATH"] = ( + ".frozen" + + os.pathsep + + base_path("../extmod") + + os.pathsep + + base_path("../lib/micropython-lib/python-stdlib/unittest") + ) try: os.makedirs(args.result_dir, exist_ok=True) - res = run_tests(pyb, tests, args, args.result_dir, args.jobs) + test_results, testcase_count = run_tests(pyb, tests, args, args.result_dir, args.jobs) + res = create_test_report(args, test_results, testcase_count) finally: if pyb: pyb.close() diff --git a/tests/serial_test.py b/tests/serial_test.py new file mode 100755 index 00000000000..eebea402fa4 --- /dev/null +++ b/tests/serial_test.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python +# +# Performance and reliability test for serial port communication. +# +# Basic usage: +# serial_test.py [-t serial-device] +# +# The `serial-device` will default to /dev/ttyACM0. + +import argparse +import random +import serial +import sys +import time + +from test_utils import test_instance_epilog, convert_device_shortcut_to_real_device + +echo_test_script = """ +import sys +bytes_min=%u +bytes_max=%u +repeat=%u +b=memoryview(bytearray(bytes_max)) +for n in range(bytes_min,bytes_max+1): + for _ in range(repeat): + n2 = sys.stdin.readinto(b[:n]) + sys.stdout.write(b[:n2]) +""" + +read_test_script = """ +bin = True +try: + wr=__import__("pyb").USB_VCP(0).send +except: + import sys + if hasattr(sys.stdout,'buffer'): + wr=sys.stdout.buffer.write + else: + wr=sys.stdout.write + bin = False +b=bytearray(%u) +if bin: + wr('BIN') + for i in range(len(b)): + b[i] = i & 0xff +else: + wr('TXT') + for i in range(len(b)): + b[i] = 0x20 + (i & 0x3f) +for _ in range(%d): + wr(b) +""" + + +write_test_script_verified = """ +import sys +try: + rd=__import__("pyb").USB_VCP(0).recv +except: + rd=sys.stdin.readinto +b=bytearray(%u) +for _ in range(%u): + n = rd(b) + fail = 0 + for i in range(n): + if b[i] != 32 + (i & 0x3f): + fail += 1 + if fail: + sys.stdout.write(b'ER%%05u' %% fail) + else: + sys.stdout.write(b'OK%%05u' %% n) +""" + +write_test_script_unverified = """ +import sys +try: + rd=__import__("pyb").USB_VCP(0).recv +except: + rd=sys.stdin.readinto +b=bytearray(%u) +for _ in range(%u): + n = rd(b) + if n != len(b): + sys.stdout.write(b'ER%%05u' %% n) + else: + sys.stdout.write(b'OK%%05u' %% n) +""" + + +class TestError(Exception): + pass + + +def drain_input(ser): + time.sleep(0.1) + while ser.inWaiting() > 0: + data = ser.read(ser.inWaiting()) + time.sleep(0.1) + + +def send_script(ser, script): + ser.write(b"\x03\x01\x04") # break, raw-repl, soft-reboot + drain_input(ser) + chunk_size = 32 + for i in range(0, len(script), chunk_size): + ser.write(script[i : i + chunk_size]) + time.sleep(0.01) + ser.write(b"\x04") # eof + ser.flush() + response = ser.read(2) + if response != b"OK": + response += ser.read(ser.inWaiting()) + raise TestError("could not send script", response) + + +def echo_test(ser_repl, ser_data): + global test_passed + + # Make the test data deterministic. + random.seed(0) + + # Set parameters for the test. + # Go just a bit above the size of a USB high-speed packet. + bytes_min = 1 + bytes_max = 520 + num_repeat = 1 + + # Load and run the write_test_script. + script = bytes(echo_test_script % (bytes_min, bytes_max, num_repeat), "ascii") + send_script(ser_repl, script) + + # A selection of printable bytes for echo data. + printable_bytes = list(range(48, 58)) + list(range(65, 91)) + list(range(97, 123)) + + # Write data to the device and record the echo'd data. + # Use a different selection of random printable characters for each + # echo, to make it easier to debug when the echo doesn't match. + num_errors = 0 + echo_results = [] + for num_bytes in range(bytes_min, bytes_max + 1): + print(f"DATA ECHO: {num_bytes} / {bytes_max}", end="\r") + for repeat in range(num_repeat): + rand_bytes = list(random.choice(printable_bytes) for _ in range(8)) + buf = bytes(random.choice(rand_bytes) for _ in range(num_bytes)) + ser_data.write(buf) + buf2 = ser_data.read(len(buf)) + match = buf == buf2 + num_errors += not match + echo_results.append((match, buf, buf2)) + if num_errors > 8: + # Stop early if there are too many errors. + break + ser_repl.write(b"\x03") + + # Print results. + if all(match for match, _, _ in echo_results): + print("DATA ECHO: OK for {}-{} bytes at a time".format(bytes_min, bytes_max)) + else: + test_passed = False + print("DATA ECHO: FAIL ") + for match, buf, buf2 in echo_results: + print(" sent", len(buf), buf) + if match: + print(" echo match") + else: + print(" echo", len(buf), buf2) + + +def read_test(ser_repl, ser_data, bufsize, nbuf): + global test_passed + + assert bufsize % 256 == 0 # for verify to work + + # how long to wait for data from device + # (if UART TX is also enabled then it can take 1.4s to send + # out a 16KB butter at 115200bps) + READ_TIMEOUT_S = 2 + + # Load and run the read_test_script. + script = bytes(read_test_script % (bufsize, nbuf), "ascii") + send_script(ser_repl, script) + + # Read from the device the type of data that it will send (BIN or TXT). + data_type = ser_data.read(3) + + # Read data from the device, check it is correct, and measure throughput. + n = 0 + last_byte = None + t_start = time.time() + remain = nbuf * bufsize + total_data = bytearray(remain) + while remain: + t0 = time.monotonic_ns() + while ser_data.inWaiting() == 0: + if time.monotonic_ns() - t0 > READ_TIMEOUT_S * 1e9: + # timeout waiting for data from device + break + time.sleep(0.0001) + if not ser_data.inWaiting(): + test_passed = False + print("ERROR: timeout waiting for data") + print(total_data[:n]) + return 0 + to_read = min(ser_data.inWaiting(), remain) + data = ser_data.read(to_read) + remain -= len(data) + print(f"{n} / {nbuf * bufsize}", end="\r") + total_data[n : n + len(data)] = data + n += len(data) + t_end = time.time() + for i in range(0, len(total_data)): + if data_type == b"BIN": + wanted = i & 0xFF + else: + wanted = 0x20 + (i & 0x3F) + if total_data[i] != wanted: + test_passed = False + print("ERROR: data mismatch:", i, wanted, total_data[i]) + ser_repl.write(b"\x03") # break + t = t_end - t_start + + # Print results. + print( + "DATA IN: bufsize=%u, read %u bytes in %.2f msec = %.2f kibytes/sec = %.2f MBits/sec" + % (bufsize, n, t * 1000, n / 1024 / t, n * 8 / 1000000 / t) + ) + + return n / t + + +def write_test(ser_repl, ser_data, bufsize, nbuf, verified): + global test_passed + + # Load and run the write_test_script. + if verified: + script = write_test_script_verified + else: + script = write_test_script_unverified + script = bytes(script % (bufsize, nbuf), "ascii") + send_script(ser_repl, script) + drain_input(ser_repl) + + # Write data to the device, check it is correct, and measure throughput. + n = 0 + t_start = time.time() + buf = bytearray(bufsize) + for i in range(len(buf)): + buf[i] = 32 + (i & 0x3F) # don't want to send ctrl chars! + for i in range(nbuf): + ser_data.write(buf) + n += len(buf) + print(f"{n} / {nbuf * bufsize}", end="\r") + response = ser_repl.read(7) + if response != b"OK%05u" % bufsize: + test_passed = False + print("ERROR: bad response, expecting OK%05u, got %r" % (bufsize, response)) + t_end = time.time() + ser_repl.write(b"\x03") # break + t = t_end - t_start + + # Print results. + print( + "DATA OUT: verify=%d, bufsize=%u, wrote %u bytes in %.2f msec = %.2f kibytes/sec = %.2f MBits/sec" + % (verified, bufsize, n, t * 1000, n / 1024 / t, n * 8 / 1000000 / t) + ) + + return n / t + + +def do_test(dev_repl, dev_data=None, time_per_subtest=1): + if dev_data is None: + print("REPL and data on", dev_repl) + ser_repl = serial.Serial(dev_repl, baudrate=115200, timeout=1) + ser_data = ser_repl + else: + print("REPL on", dev_repl) + print("data on", dev_data) + ser_repl = serial.Serial(dev_repl, baudrate=115200, timeout=1) + ser_data = serial.Serial(dev_data, baudrate=115200, timeout=1) + + # Do echo test first, and abort if it doesn't pass. + echo_test(ser_repl, ser_data) + if not test_passed: + return + + # Do read and write throughput test. + for test_func, test_args, bufsize in ( + (read_test, (), 256), + (write_test, (True,), 128), + (write_test, (False,), 128), + ): + nbuf = 128 + while bufsize <= 16384: + rate = test_func(ser_repl, ser_data, bufsize, nbuf, *test_args) + bufsize *= 2 + if rate: + # Adjust the amount of data based on the rate, to keep each subtest + # at around time_per_subtest seconds long. + nbuf = max(min(128, int(rate * time_per_subtest / bufsize)), 1) + + ser_repl.close() + ser_data.close() + + +def main(): + global test_passed + + cmd_parser = argparse.ArgumentParser( + description="Test performance and reliability of serial port communication.", + epilog=test_instance_epilog, + formatter_class=argparse.RawTextHelpFormatter, + ) + cmd_parser.add_argument( + "-t", + "--test-instance", + default="a0", + help="MicroPython instance to test", + ) + cmd_parser.add_argument( + "--time-per-subtest", default="1", help="approximate time to take per subtest (in seconds)" + ) + args = cmd_parser.parse_args() + + dev_repl = convert_device_shortcut_to_real_device(args.test_instance) + + test_passed = True + try: + do_test(dev_repl, None, float(args.time_per_subtest)) + except TestError as er: + test_passed = False + print("ERROR:", er) + + if not test_passed: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tests/stress/bytecode_limit.py b/tests/stress/bytecode_limit.py index 948d7668da5..0a72b66fa05 100644 --- a/tests/stress/bytecode_limit.py +++ b/tests/stress/bytecode_limit.py @@ -1,19 +1,29 @@ # Test the limits of bytecode generation. +import sys + +# Tune the test parameters based on the target's bytecode generator. +if hasattr(sys.implementation, "_mpy"): + # Target can load .mpy files so generated bytecode uses 1 byte per qstr. + number_of_body_copies = (433, 432, 431, 399) +else: + # Target can't load .mpy files so generated bytecode uses 2 bytes per qstr. + number_of_body_copies = (401, 400, 399, 398) + body = " with f()()() as a:\n try:\n f()()()\n except Exception:\n pass\n" # Test overflow of jump offset. # Print results at the end in case an intermediate value of n fails with MemoryError. results = [] -for n in (433, 432, 431, 430): +for n in number_of_body_copies: try: exec("cond = 0\nif cond:\n" + body * n + "else:\n print('cond false')\n") - results.append((n, "ok")) + results.append("ok") except MemoryError: print("SKIP") raise SystemExit - except RuntimeError: - results.append((n, "RuntimeError")) + except RuntimeError as er: + results.append(repr(er)) print(results) # Test changing size of code info (source line/bytecode mapping) due to changing diff --git a/tests/stress/bytecode_limit.py.exp b/tests/stress/bytecode_limit.py.exp index cda52b1b973..50511665f00 100644 --- a/tests/stress/bytecode_limit.py.exp +++ b/tests/stress/bytecode_limit.py.exp @@ -1,4 +1,4 @@ cond false cond false -[(433, 'RuntimeError'), (432, 'RuntimeError'), (431, 'ok'), (430, 'ok')] +["RuntimeError('bytecode overflow',)", "RuntimeError('bytecode overflow',)", 'ok', 'ok'] [123] diff --git a/tests/stress/dict_copy.py b/tests/stress/dict_copy.py index 73d3a5b51d6..f9b742e20f7 100644 --- a/tests/stress/dict_copy.py +++ b/tests/stress/dict_copy.py @@ -1,6 +1,11 @@ # copying a large dictionary -a = {i: 2 * i for i in range(1000)} +try: + a = {i: 2 * i for i in range(1000)} +except MemoryError: + print("SKIP") + raise SystemExit + b = a.copy() for i in range(1000): print(i, b[i]) diff --git a/tests/stress/dict_create.py b/tests/stress/dict_create.py index e9db40a8e6c..91a83a12f9d 100644 --- a/tests/stress/dict_create.py +++ b/tests/stress/dict_create.py @@ -3,6 +3,10 @@ d = {} x = 1 while x < 1000: - d[x] = x + try: + d[x] = x + except MemoryError: + print("SKIP") + raise SystemExit x += 1 print(d[500]) diff --git a/tests/stress/fun_call_limit.py b/tests/stress/fun_call_limit.py index b802aadd558..69f8aa5aec4 100644 --- a/tests/stress/fun_call_limit.py +++ b/tests/stress/fun_call_limit.py @@ -16,14 +16,16 @@ def test(n): # If the port has at least 32-bits then this test should pass. -print(test(29)) +print(test(28)) # This test should fail on all ports (overflows a small int). print(test(70)) -# Check that there is a correct transition to the limit of too many args before *args. +# 28 is the biggest number that will pass on a 32-bit port using object +# representation B, which has 1<<28 still fitting in a positive small int. reached_limit = False -for i in range(30, 70): +any_test_succeeded = False +for i in range(28, 70): result = test(i) if reached_limit: if result != "SyntaxError": @@ -34,3 +36,5 @@ def test(n): else: if result != i + 4: print("FAIL") + any_test_succeeded = True +assert any_test_succeeded # At least one iteration must have passed diff --git a/tests/stress/fun_call_limit.py.exp b/tests/stress/fun_call_limit.py.exp index 53d2b280430..491abbfa8be 100644 --- a/tests/stress/fun_call_limit.py.exp +++ b/tests/stress/fun_call_limit.py.exp @@ -1,2 +1,2 @@ -33 +32 SyntaxError diff --git a/tests/stress/qstr_limit.py b/tests/stress/qstr_limit.py index 08b10a039f5..c7bd437f3ad 100644 --- a/tests/stress/qstr_limit.py +++ b/tests/stress/qstr_limit.py @@ -12,8 +12,8 @@ def make_id(n, base="a"): var = make_id(l) try: exec(var + "=1", g) - except RuntimeError: - print("RuntimeError", l) + except RuntimeError as er: + print("RuntimeError", er, l) continue print(var in g) @@ -26,16 +26,16 @@ def f(**k): for l in range(254, 259): try: exec("f({}=1)".format(make_id(l))) - except RuntimeError: - print("RuntimeError", l) + except RuntimeError as er: + print("RuntimeError", er, l) # type construction for l in range(254, 259): id = make_id(l) try: - print(type(id, (), {}).__name__) - except RuntimeError: - print("RuntimeError", l) + print(type(id, (), {})) + except RuntimeError as er: + print("RuntimeError", er, l) # hasattr, setattr, getattr @@ -48,28 +48,20 @@ class A: a = A() try: setattr(a, id, 123) - except RuntimeError: - print("RuntimeError", l) + except RuntimeError as er: + print("RuntimeError", er, l) try: print(hasattr(a, id), getattr(a, id)) - except RuntimeError: - print("RuntimeError", l) + except RuntimeError as er: + print("RuntimeError", er, l) # format with keys for l in range(254, 259): id = make_id(l) try: print(("{" + id + "}").format(**{id: l})) - except RuntimeError: - print("RuntimeError", l) - -# modulo format with keys -for l in range(254, 259): - id = make_id(l) - try: - print(("%(" + id + ")d") % {id: l}) - except RuntimeError: - print("RuntimeError", l) + except RuntimeError as er: + print("RuntimeError", er, l) # import module # (different OS's have different results so only run those that are consistent) @@ -78,8 +70,8 @@ class A: __import__(make_id(l)) except ImportError: print("ok", l) - except RuntimeError: - print("RuntimeError", l) + except RuntimeError as er: + print("RuntimeError", er, l) # import package for l in (100, 101, 102, 128, 129): @@ -87,5 +79,5 @@ class A: exec("import " + make_id(l) + "." + make_id(l, "A")) except ImportError: print("ok", l) - except RuntimeError: - print("RuntimeError", l) + except RuntimeError as er: + print("RuntimeError", er, l) diff --git a/tests/stress/qstr_limit.py.exp b/tests/stress/qstr_limit.py.exp index 455761bc71e..2349adf220f 100644 --- a/tests/stress/qstr_limit.py.exp +++ b/tests/stress/qstr_limit.py.exp @@ -1,43 +1,38 @@ True True -RuntimeError 256 -RuntimeError 257 -RuntimeError 258 +RuntimeError name too long 256 +RuntimeError name too long 257 +RuntimeError name too long 258 {'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrst': 1} {'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstu': 1} -RuntimeError 256 -RuntimeError 257 -RuntimeError 258 -abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrst -abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstu -RuntimeError 256 -RuntimeError 257 -RuntimeError 258 +RuntimeError name too long 256 +RuntimeError name too long 257 +RuntimeError name too long 258 + + +RuntimeError name too long 256 +RuntimeError name too long 257 +RuntimeError name too long 258 True 123 True 123 -RuntimeError 256 -RuntimeError 256 -RuntimeError 257 -RuntimeError 257 -RuntimeError 258 -RuntimeError 258 +RuntimeError name too long 256 +RuntimeError name too long 256 +RuntimeError name too long 257 +RuntimeError name too long 257 +RuntimeError name too long 258 +RuntimeError name too long 258 254 255 -RuntimeError 256 -RuntimeError 257 -RuntimeError 258 -254 -255 -RuntimeError 256 -RuntimeError 257 -RuntimeError 258 +RuntimeError name too long 256 +RuntimeError name too long 257 +RuntimeError name too long 258 ok 100 ok 101 -RuntimeError 256 -RuntimeError 257 -RuntimeError 258 +RuntimeError name too long 256 +RuntimeError name too long 257 +RuntimeError name too long 258 ok 100 ok 101 ok 102 -RuntimeError 128 -RuntimeError 129 +RuntimeError name too long 128 +RuntimeError name too long 129 diff --git a/tests/stress/qstr_limit_str_modulo.py b/tests/stress/qstr_limit_str_modulo.py new file mode 100644 index 00000000000..90b9f4364ec --- /dev/null +++ b/tests/stress/qstr_limit_str_modulo.py @@ -0,0 +1,21 @@ +# Test interning qstrs that go over the qstr length limit (255 bytes in default configuration). +# The tests here are specifically for str formatting with %. + +try: + "" % () +except TypeError: + print("SKIP") + raise SystemExit + + +def make_id(n, base="a"): + return "".join(chr(ord(base) + i % 26) for i in range(n)) + + +# modulo format with keys +for l in range(254, 259): + id = make_id(l) + try: + print(("%(" + id + ")d") % {id: l}) + except RuntimeError as er: + print("RuntimeError", er, l) diff --git a/tests/stress/qstr_limit_str_modulo.py.exp b/tests/stress/qstr_limit_str_modulo.py.exp new file mode 100644 index 00000000000..3632c85bffe --- /dev/null +++ b/tests/stress/qstr_limit_str_modulo.py.exp @@ -0,0 +1,5 @@ +254 +255 +RuntimeError name too long 256 +RuntimeError name too long 257 +RuntimeError name too long 258 diff --git a/tests/stress/recursive_iternext.py b/tests/stress/recursive_iternext.py index bbc389e7262..c737f1e36d7 100644 --- a/tests/stress/recursive_iternext.py +++ b/tests/stress/recursive_iternext.py @@ -1,4 +1,8 @@ # This tests that recursion with iternext doesn't lead to segfault. +# +# This test segfaults CPython, but that's not a bug as CPython doesn't enforce +# limits on C recursion - see +# https://github.com/python/cpython/issues/58218#issuecomment-1093570209 try: enumerate filter @@ -9,49 +13,25 @@ print("SKIP") raise SystemExit -# We need to pick an N that is large enough to hit the recursion -# limit, but not too large that we run out of heap memory. -try: - # large stack/heap, eg unix - [0] * 80000 - N = 5000 -except: - try: - # medium, eg pyboard - [0] * 10000 - N = 1000 - except: - # small, eg esp8266 - N = 100 - -try: - x = (1, 2) - for i in range(N): - x = enumerate(x) - tuple(x) -except RuntimeError: - print("RuntimeError") -try: +# Progressively build a bigger nested iterator structure (10 at a time for speed), +# and then try to evaluate it via tuple(x) which makes deep recursive function calls. +# +# Eventually this should raise a RuntimeError as MicroPython runs out of stack. +# It shouldn't ever raise a MemoryError, if it does then somehow MicroPython has +# run out of heap (for the nested structure) before running out of stack. +def recurse_iternext(nested_fn): x = (1, 2) - for i in range(N): - x = filter(None, x) - tuple(x) -except RuntimeError: - print("RuntimeError") + while True: + for _ in range(10): + x = nested_fn(x) + try: + tuple(x) + except RuntimeError: + print("RuntimeError") + break -try: - x = (1, 2) - for i in range(N): - x = map(max, x, ()) - tuple(x) -except RuntimeError: - print("RuntimeError") -try: - x = (1, 2) - for i in range(N): - x = zip(x) - tuple(x) -except RuntimeError: - print("RuntimeError") +# Test on various nested iterator structures +for nested_fn in [enumerate, lambda x: filter(None, x), lambda x: map(max, x, ()), zip]: + recurse_iternext(nested_fn) diff --git a/tests/target_wiring/EK_RA6M2.py b/tests/target_wiring/EK_RA6M2.py new file mode 100644 index 00000000000..7d4a8cbbd64 --- /dev/null +++ b/tests/target_wiring/EK_RA6M2.py @@ -0,0 +1,8 @@ +# Target wiring for EK_RA6M2. +# +# Connect: +# - P601 to P602 + +# UART(9) is on P602/P601. +uart_loopback_args = (9,) +uart_loopback_kwargs = {} diff --git a/tests/target_wiring/NUCLEO_WB55.py b/tests/target_wiring/NUCLEO_WB55.py new file mode 100644 index 00000000000..e40d35e225b --- /dev/null +++ b/tests/target_wiring/NUCLEO_WB55.py @@ -0,0 +1,12 @@ +# Target wiring for NUCLEO_WB55. +# +# Connect: +# - PA2 to PA3 + +# LPUART(1) is on PA2/PA3. +uart_loopback_args = ("LP1",) +uart_loopback_kwargs = {} + +spi_standalone_args_list = [(1,), (2,)] + +pwm_loopback_pins = [("D1", "D0")] diff --git a/tests/target_wiring/PYBx.py b/tests/target_wiring/PYBx.py new file mode 100644 index 00000000000..b36e342e944 --- /dev/null +++ b/tests/target_wiring/PYBx.py @@ -0,0 +1,16 @@ +# Target wiring for PYBV10, PYBV11, PYBLITEV10, PYBD_SF2, PYBD_SF3, PYBD_SF6. +# +# Connect: +# - X1 to X2 + +# UART("XA") is on X1/X2 (usually UART(4) on PA0/PA1). +uart_loopback_args = ("XA",) +uart_loopback_kwargs = {} + +spi_standalone_args_list = [(1,), (2,)] + +# CAN args assume no connection for single device tests +can_args = (1,) +can_kwargs = {} + +pwm_loopback_pins = [("X1", "X2")] diff --git a/tests/target_wiring/README.md b/tests/target_wiring/README.md new file mode 100644 index 00000000000..c3a7038bb91 --- /dev/null +++ b/tests/target_wiring/README.md @@ -0,0 +1,81 @@ +# Target wiring + +Some tests require hardware configuration and/or external connections, for example +bridging a pair of GPIO pins. Each board that such tests run on needs to be configured +individually. That is achieved by providing a target wiring configuration script that +defines the necessary hardware parameters for each test. + +## Selecting the target wiring + +There are three ways to provide the target wiring configuration: + +1. Specify it explicitly when running the test: `./run-tests.py --target-wiring