From 237b01877b97c48c02131928fff4633520e7d402 Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Tue, 26 Sep 2023 14:56:21 -0500 Subject: [PATCH] chore(qol): Test python and shell code samples. WIP - use for local testing only, not ready for CI. (#5154) - Adds a Docker image, scripts, and configuration that install and run pytest-codeblocks on Python and shell code samples in Markdown. - Copied from https://github.com/jstirnaman/docs-codeblocks/ - TODO: The script copies files into a tmp directory configured as a bind mount for the Docker container. We might want to replace this with the experimental `watch` feature. - TODO: Use a secret to configure the token. As is, values subbed from the dotenv file are exposed in your Docker container history. --- Dockerfile | 47 ++++++++ compose.yaml | 17 +++ .../best-practices/optimize-writes.md | 2 + test.sh | 43 +++++++ test/.dockerignore | 35 ++++++ test/.gitignore | 7 ++ test/README.md | 105 ++++++++++++++++++ test/package.json | 18 +++ test/pytest.ini | 11 ++ test/requirements.txt | 8 ++ test/run-tests.sh | 15 +++ 11 files changed, 308 insertions(+) create mode 100644 Dockerfile create mode 100644 compose.yaml create mode 100755 test.sh create mode 100644 test/.dockerignore create mode 100644 test/.gitignore create mode 100644 test/README.md create mode 100644 test/package.json create mode 100644 test/pytest.ini create mode 100644 test/requirements.txt create mode 100644 test/run-tests.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..e9729b0bc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +# syntax=docker/dockerfile:1 + +# If you need more help, visit the Dockerfile reference guide at +# https://docs.docker.com/engine/reference/builder/ + +ARG PYTHON_VERSION=3.11.5 + +FROM python:${PYTHON_VERSION}-slim as base + +RUN apt-get update && apt-get install -y \ + curl \ + git \ + && rm -rf /var/lib/apt/lists/* + +ARG SOURCE_DIR +ARG TESTS_DIR + +# Prevents Python from writing pyc files. +ENV PYTHONDONTWRITEBYTECODE=1 + +# Keeps Python from buffering stdout and stderr to avoid situations where +# the application crashes without emitting any logs due to buffering. +ENV PYTHONUNBUFFERED=1 + +COPY $SOURCE_DIR /app/$SOURCE_DIR + +WORKDIR /app/$SOURCE_DIR +RUN chmod -R 755 /app/$SOURCE_DIR + +COPY $SOURCE_DIR/run-tests.sh /usr/local/bin/run-tests.sh +RUN chmod +x /usr/local/bin/run-tests.sh + +# Download dependencies as a separate step to take advantage of Docker's caching. +# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. +# Leverage a bind mount to requirements.txt to avoid having to copy them into +# into this layer. +RUN --mount=type=cache,target=/root/.cache/pip \ + --mount=type=bind,source=${SOURCE_DIR}/requirements.txt,target=requirements.txt \ + python -m pip install -r requirements.txt + +# RUN --mount=type=cache,target=/root/.cache/node_modules \ +# --mount=type=bind,source=package.json,target=package.json \ +# npm install + +WORKDIR $TESTS_DIR +ENTRYPOINT ["run-tests.sh"] +CMD [""] diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 000000000..97f9f47ec --- /dev/null +++ b/compose.yaml @@ -0,0 +1,17 @@ +# For examples, see the Awesome Compose repository: +# https://github.com/docker/awesome-compose +services: + test: + image: docs-v2-tests + env_file: + - ./test/.env.influxdbv3 + volumes: + - type: bind + source: ./test/tmp + target: /app/test/tmp + build: + context: . + dockerfile: Dockerfile + args: + - SOURCE_DIR=test + - TESTS_DIR=tmp diff --git a/content/influxdb/cloud-serverless/write-data/best-practices/optimize-writes.md b/content/influxdb/cloud-serverless/write-data/best-practices/optimize-writes.md index c8926bfca..822a6ee2f 100644 --- a/content/influxdb/cloud-serverless/write-data/best-practices/optimize-writes.md +++ b/content/influxdb/cloud-serverless/write-data/best-practices/optimize-writes.md @@ -44,6 +44,8 @@ whichever threshold is met first. Before writing data points to InfluxDB, sort tags by key in lexicographic order. _Verify sort results match results from the [Go `bytes.Compare` function](http://golang.org/pkg/bytes/#Compare)._ + + ```sh # Line protocol example with unsorted tags measurement,tagC=therefore,tagE=am,tagA=i,tagD=i,tagB=think fieldKey=fieldValue 1562020262 diff --git a/test.sh b/test.sh new file mode 100755 index 000000000..d1818655d --- /dev/null +++ b/test.sh @@ -0,0 +1,43 @@ +#! /bin/bash + +# Path: test.sh +# Description: +# This script is used to copy content files for testing and to run tests on tests on those temporary copies. +# The temporary files are shared between the host and the Docker container +# using a bind mount configured in compose.yaml. +# +# Docker compose now has an experimental file watch feature +# (https://docs.docker.com/compose/file-watch/) that is likely preferable to the +# strategy here. +# +# Usage: +# The default behavior is to test all *.md files that have been added or modified in the current branch, effectively: +# +# `git diff --name-only --diff-filter=AM --relative master | grep -E '\.md$' | ./test.sh` +# +# To specify files to test, in your terminal command line, pass a file pattern as the only argument to the script--for example: +# +# sh test.sh ./content/**/*.md +## + +paths="$1" +target=./test/tmp +testrun=./test/.test-run.txt +mkdir -p "$target" +cat /dev/null > "$testrun" +rm -rf "$target"/* + +# Check if the user provided a path to copy. +if [ -z "$paths" ]; then + echo "No path provided. Running tests for *.md files that have been added or modified in the current branch." + paths=$(git diff --name-only --diff-filter=AM --relative master | \ + grep -E '\.md$') +else + paths=$(find "$paths" -type f -name '*.md') +fi + +# Log the list of files to be tested and copy them to the test directory. +echo "$paths" >> "$testrun" +echo "$paths" | rsync -arv --files-from=- . "$target" +# Start a new container and run the tests. +docker compose run --no-TTY test diff --git a/test/.dockerignore b/test/.dockerignore new file mode 100644 index 000000000..e18396c45 --- /dev/null +++ b/test/.dockerignore @@ -0,0 +1,35 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/engine/reference/builder/#dockerignore-file + +**/.DS_Store +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.env.influxdbv3 +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 000000000..069e8a3cd --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,7 @@ +/target +/Cargo.lock +node_modules +.env* +.pytest_cache +.test-run.txt +tmp diff --git a/test/README.md b/test/README.md new file mode 100644 index 000000000..d736e861e --- /dev/null +++ b/test/README.md @@ -0,0 +1,105 @@ +Test code blocks in Markdown files. + +This project contains the following: + +- `test.sh`: The primary entrypoint for running tests. + Copies Markdown files to a temporary directory shared with the `test` Docker image and runs the test container. +- `test/run-tests.sh`: The Docker image entrypoint. + Substitutes placeholders with environment variables in code blocks. + Passes test files to test runners (for example, `pytest --codeblocks` for Python and shell code samples). +- `compose.yaml` and `Dockerfile`: Docker image for the **test** service that installs test dependencies and passes test files to test runners. + +## Set configuration values + +To set your InfluxDB credentials for testing, create the file `.env.influxdbv3` and add key=value properties for the following: + +```text +INFLUX_HOST=https://us-east-1-1.aws.cloud2.influxdata.com +INFLUX_TOKEN=3S3SFrpFbnNR_pZ3Cr6LMN...== +INFLUX_ORG=28d1f2f......... +INFLUX_DATABASE=get-started +``` + +Storing configuration properties in a `.env` (dotenv) file is generally preferable to using environment variables. + +## Build the image + +1. Install Docker for your system. + +2. Build the Docker image. + + ```shell + docker compose build test + ``` + + After editing configuration or files used by the image, re-run the preceding build command. + +## Run tests + +Test code blocks in Markdown files that have changed relative to your git `master` branch: + +```sh +sh test.sh +``` + +Test code blocks in files that match a pattern: + +```sh +sh test.sh ./content/**/*.md +``` + +`test.sh` copies files into `./test/tmp/` for testing and runs the tests in Docker. + +### Test runners + +_Experimental--work in progress_ + +[pytest-codeblocks](https://github.com/nschloe/pytest-codeblocks/tree/main) extracts code from python and shell Markdown code blocks and executes assertions for the code. +If you don't assert a value, `--codeblocks` considers a non-zero exit code to be a failure. +_Note_: `pytest --codeblocks` uses Python's `subprocess.run()` to execute shell code. + +To assert (and display) the expected output of your code, follow the code block with the `` comment tag, and then the expected output in a code block--for example: + +```python +print("Hello, world!") +``` + + + +If successful, the output is the following: + +``` +Hello, world! +``` + +pytest-codeblocks has features for skipping tests and marking blocks as failed. +To learn more, see the pytest-codeblocks README and tests. + +#### Other tools and ideas for testing code blocks + +The `codedown` NPM package extracts code from Markdown code blocks for each language and +can pipe the output to a test runner for the language. + +`pytest` and `pytest-codeblocks` use the Python `Assertions` module to keep testing overhead low. +Node.js also provides an `Assertions` package. + +The `runmd` NPM package runs `javascript` code blocks in Markdown and generates a new Markdown file with the code block output inserted. + +## Troubleshoot + +### Pytest collected 0 items + +Potential reasons: + +- See the test discovery options in `pytest.ini`. +- For Python code blocks, use the following delimiter: + + ```python + # Codeblocks runs this block. + ``` + + `pytest --codeblocks` ignores code blocks that use the following: + + ```py + # Codeblocks ignores this block. + ``` \ No newline at end of file diff --git a/test/package.json b/test/package.json new file mode 100644 index 000000000..2d9d40f37 --- /dev/null +++ b/test/package.json @@ -0,0 +1,18 @@ +{ + "name": "docs-codesamples", + "version": "1.0.0", + "description": "", + "main": "index.js", + "directories": { + "example": "examples" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "runmd": "^1.3.9" + } +} diff --git a/test/pytest.ini b/test/pytest.ini new file mode 100644 index 000000000..79e51cf9b --- /dev/null +++ b/test/pytest.ini @@ -0,0 +1,11 @@ +# pytest configuration file +[pytest] +# Collect tests. +# Collect tests from files matching file name pattern. +python_files = *_test.py *_test_sh.py +# Collect classes with names ending in Test. +python_classes = *Test +# Collect all functions. +python_functions = * + +filterwarnings = ignore::pytest.PytestReturnNotNoneWarning \ No newline at end of file diff --git a/test/requirements.txt b/test/requirements.txt new file mode 100644 index 000000000..d8896ba31 --- /dev/null +++ b/test/requirements.txt @@ -0,0 +1,8 @@ +## Code sample dependencies +influxdb3-python +pandas +python-dotenv==1.0.0 +## Test dependencies +pytest==7.4.1 +pytest-cov==2.12.1 +pytest-codeblocks==0.16.1 \ No newline at end of file diff --git a/test/run-tests.sh b/test/run-tests.sh new file mode 100644 index 000000000..eb1c15562 --- /dev/null +++ b/test/run-tests.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +for file in "$TEST_DIR"/*; do + if [ -f "$file" ]; then + # Replace placeholder values with environment variables. + sed -i '' 's|https:\/\/{{< influxdb/host >}}|$INFLUX_HOST|g; + s/API_TOKEN/$INFLUX_TOKEN/g; + s/DATABASE_TOKEN/$INFLUX_TOKEN/g; + s/BUCKET_NAME/$INFLUX_DATABASE/g; + s/DATABASE_NAME/$INFLUX_DATABASE/g;' \ + $file + fi +done + +pytest --codeblocks $TEST_DIR