fix(ci): Simplify Vale terms list, support OAuth browser flow in tests

- For influxctl OAuth flow, add support for writing auth URLs to a container-shared file monitored by the host during the pre-commit hook.
- In prepare-content.sh, add Management API-associated placeholder substitutions.
- Update CONTRIBUTING.md with env.test requirements.
pull/5505/head
Jason Stirnaman 2024-07-01 09:52:07 -05:00
parent 118f692065
commit 9c035fbd4d
13 changed files with 324 additions and 167 deletions

View File

@ -1 +1,3 @@
./test/src/monitor-tests.sh start
npx lint-staged --relative
./test/src/monitor-tests.sh kill

View File

@ -1,6 +1,6 @@
// Lint-staged configuration. This file must export a lint-staged configuration object.
function testStagedContent(paths, productPath) {
function pytestStagedContent(paths, productPath) {
const productName = productPath.replace(/\//g, '-');
const CONTENT = `staged-${productName}`;
const TEST = `pytest-${productName}`;
@ -26,7 +26,7 @@ function testStagedContent(paths, productPath) {
`docker run --name ${CONTENT}
--label tag=influxdata-docs
--label stage=test
--mount type=volume,source=staged-content,target=/app/content
--mount type=volume,source=${CONTENT},target=/app/content
--mount type=bind,src=./content,dst=/src/content
--mount type=bind,src=./static/downloads,dst=/app/data
influxdata-docs/tests --files "${paths.join(' ')}"`,
@ -36,28 +36,23 @@ function testStagedContent(paths, productPath) {
-t influxdata-docs/pytest:latest`,
// Run test runners.
// This script first checks if there are any tests to run using `pytest --collect-only`.
// If there are tests, it runs them; otherwise, it exits with a success code.
// Uses a pytest plugin to suppress exit code 5 (if no tests are found),
// This avoids needing to "pre-run" test collection in a subshell to check the exit code.
// Instead of the plugin, we could use a placeholder test that always or conditionally passes.
// Whether tests pass or fail, the container is removed,
// but the CONTENT container and associated volume will remain until the next run.
`sh -c "docker run --rm --name ${TEST}-collector \
// Note: the "--network host" setting and `host-open` script are used to
// forward influxctl authentication URLs from the container to the host
// where they can be opened and approved in a host browser.
// Allowing "--network host" has security implications and isn't ideal.
`docker run --rm -t \
--label tag=influxdata-docs \
--label stage=test \
--name ${TEST} \
--env-file ${productPath}/.env.test \
--volumes-from ${CONTENT} \
influxdata-docs/pytest --codeblocks --collect-only \
${productPath}/ > /dev/null 2>&1; \
TEST_COLLECT_EXIT_CODE=$?; \
if [ $TEST_COLLECT_EXIT_CODE -eq 5 ]; then \
echo 'No tests to run.'; \
exit 0; \
else \
docker run --rm \
--label tag=influxdata-docs \
--label stage=test \
--name ${TEST} \
--env-file ${productPath}/.env.test \
--volumes-from ${CONTENT} \
influxdata-docs/pytest --codeblocks --exitfirst ${productPath}/;
fi"`
--mount type=bind,src=./test/shared,dst=/shared \
influxdata-docs/pytest --codeblocks --suppress-no-test-exit-code --exitfirst ${productPath}/`,
];
}
@ -74,42 +69,42 @@ export default {
"content/influxdb/cloud/**/*.md":
paths => [
`.ci/vale/vale.sh --config .vale.ini --minAlertLevel error ${paths}`,
...testStagedContent(paths, 'content/influxdb/cloud'),
...pytestStagedContent(paths, 'content/influxdb/cloud'),
],
"content/influxdb/cloud-dedicated/**/*.md":
"content/influxdb/cloud-dedicated/**/*.md":
paths => [
`.ci/vale/vale.sh --config content/influxdb/cloud-dedicated/.vale.ini --minAlertLevel error ${paths}`,
...testStagedContent(paths, 'content/influxdb/cloud-dedicated'),
],
`.ci/vale/vale.sh --config content/influxdb/cloud-dedicated/.vale.ini --minAlertLevel error ${paths}`,
...pytestStagedContent(paths, 'content/influxdb/cloud-dedicated'),
],
"content/influxdb/cloud-serverless/**/*.md":
paths => [
`.ci/vale/vale.sh --config content/influxdb/cloud-serverless/.vale.ini --minAlertLevel error ${paths}`,
...testStagedContent(paths, 'content/influxdb/cloud-serverless'),
...pytestStagedContent(paths, 'content/influxdb/cloud-serverless'),
],
"content/influxdb/clustered/**/*.md":
paths => [
`.ci/vale/vale.sh --config content/influxdb/clustered/.vale.ini --minAlertLevel error ${paths}`,
...testStagedContent(paths, 'content/influxdb/clustered'),
...pytestStagedContent(paths, 'content/influxdb/clustered'),
],
"content/influxdb/v1/**/*.md":
paths => [
`.ci/vale/vale.sh --config .vale.ini --minAlertLevel error ${paths}`,
...testStagedContent(paths, 'content/influxdb/v1'),
...pytestStagedContent(paths, 'content/influxdb/v1'),
],
"content/influxdb/v2/**/*.md":
paths => [
`.ci/vale/vale.sh --config .vale.ini --minAlertLevel error ${paths}`,
...testStagedContent(paths, 'content/influxdb/v2'),
...pytestStagedContent(paths, 'content/influxdb/v2'),
],
"content/telegraf/**/*.md":
paths => [
`.ci/vale/vale.sh --config .vale.ini --minAlertLevel error ${paths}`,
...testStagedContent(paths, 'content/telegraf'),
...pytestStagedContent(paths, 'content/telegraf'),
],
}

View File

@ -85,40 +85,179 @@ git commit -m "<COMMIT_MESSAGE>" --no-verify
For more options, see the [Husky documentation](https://typicode.github.io/husky/how-to.html#skipping-git-hooks).
### Configure test credentials
### Set up test scripts and credentials
To configure credentials for tests, set the usual InfluxDB environment variables
for each product inside a `content/influxdb/<PRODUCT_DIRECTORY>/.env.test` file.
To set up your docs-v2 instance to run tests locally, do the following:
The Docker commands in the `.lintstagedrc.mjs` lint-staged configuration load
the `.env.test` as product-specific environment variables.
1. **Set executable permissions on test scripts** in `./test/src`:
**Warning**: To prevent accidentally adding credentials to the docs-v2 repo,
```sh
chmod +x ./test/src/*.sh
```
2. **Create credentials for tests**:
- Create databases, buckets, and tokens for the product(s) you're testing.
- If you don't have access to a Clustered instance, you can use your
Cloud Dedicated instance for testing in most cases. To avoid conflicts when
running tests, create separate Cloud Dedicated and Clustered databases.
3. **Create .env.test**: Copy the `./test/env.test.example` file into each
product directory to test and rename the file as `.env.test`--for example:
```sh
./content/influxdb/cloud-dedicated/.env.test
```
4. Inside each product's `.env.test` file, assign your InfluxDB credentials to
environment variables.
In addition to the usual `INFLUX_` environment variables, in your
`cloud-dedicated/.env.test` and `clustered/.env.test` files define the
following variables:
- `ACCOUNT_ID`, `CLUSTER_ID`: You can find these values in your `influxctl`
`config.toml` configuration file.
- `MANAGEMENT_TOKEN`: Use the `influxctl management create` command to generate
a long-lived management token to authenticate Management API requests
For the full list of variables you'll need to include, see the substitution
patterns in `./test/src/prepare-content.sh`.
**Warning**: The database you configure in `.env.test` and any written data may
be deleted during test runs.
**Warning**: To prevent accidentally adding credentials to the docs-v2 repo,
Git is configured to ignore `.env*` files. Don't add your `.env.test` files to Git.
Consider backing them up on your local machine in case of accidental deletion.
5. For influxctl commands to run in tests, move or copy your `config.toml` file
to the `./test` directory.
### Pre-commit linting and testing
When you try to commit your changes using `git commit` or your editor,
the project automatically runs pre-commit checks for spelling, punctuation,
and style on your staged files.
The pre-commit hook calls [`lint-staged`](https://github.com/lint-staged/lint-staged) using the configuration in `.lintstagedrc.mjs`.
`.husky/pre-commit` script runs Git pre-commit hook commands, including
[`lint-staged`](https://github.com/lint-staged/lint-staged).
To run `lint-staged` scripts manually (without committing), enter the following
command in your terminal:
The `.lintstagedrc.mjs` lint-staged configuration maps product-specific glob
patterns to lint and test commands and passes a product-specific
`.env.test` file to a test runner Docker container.
The container then loads the `.env` file into the container's environment variables.
```sh
npx lint-staged --relative --verbose
```
To test or troubleshoot testing and linting scripts and configurations before
committing, choose from the following:
- To run pre-commit scripts without actually committing, append `exit 1` to the
`.husky/pre-commit` script--for example:
```sh
./test/src/monitor-tests.sh start
npx lint-staged --relative
./test/src/monitor-tests.sh kill
exit 1
```
And then run `git commit`.
The `exit 1` status fails the commit, even if all the tasks succeed.
- Use `yarn` to run one of the lint or test scripts configured in
`package.json`--for example:
```sh
yarn run test
```
- Run `lint-staged` directly and specify options:
```sh
npx lint-staged --relative --verbose
```
The pre-commit linting configuration checks for _error-level_ problems.
An error-level rule violation fails the commit and you must
fix the problems before you can commit your changes.
An error-level rule violation fails the commit and you must do one of the following before you can commit your changes:
If an error doesn't warrant a fix (for example, a term that should be allowed),
you can override the check and try the commit again or you can edit the linter
style rules to permanently allow the content. See **Configure style rules**.
- fix the reported problem in the content
- edit the linter rules to permanently allow the content.
See **Configure style rules**.
- temporarily override the hook (using `git commit --no-verify`)
#### Test shell and python code blocks
[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 (using a Python `assert` statement), `--codeblocks` considers a non-zero exit code to be a failure.
**Note**: `pytest --codeblocks` uses Python's `subprocess.run()` to execute shell code.
You can use this to test CLI and interpreter commands, regardless of programming
language, as long as they return standard exit codes.
To make the documented output of a code block testable, precede it with the
`<!--pytest-codeblocks:expected-output-->` tag and **omit the code block language
descriptor**--for example, in your Markdown file:
##### Example markdown
```python
print("Hello, world!")
```
<!--pytest-codeblocks:expected-output-->
The next code block is treated as an assertion.
If successful, the output is the following:
```
Hello, world!
```
For commands, such as `influxctl` CLI commands, that require launching an
OAuth URL in a browser, wrap the command in a subshell and redirect the output
to `/shared/urls.txt` in the container--for example:
```sh
# Test the preceding command outside of the code block.
# influxctl authentication requires TTY interaction--
# output the auth URL to a file that the host can open.
script -c "influxctl user list " \
/dev/null > /shared/urls.txt
```
You probably don't want to display this syntax in the docs, which unfortunately
means you'd need to include the test block separately from the displayed code
block.
To hide it from users, wrap the code block inside an HTML comment.
Pytest-codeblocks will still collect and run the code block.
##### Mark tests to skip
pytest-codeblocks has features for skipping tests and marking blocks as failed.
To learn more, see the pytest-codeblocks README and tests.
#### Troubleshoot tests
### 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.
```
### Vale style linting

View File

@ -26,10 +26,20 @@ ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Create a mock xdg-open script` to prevent the test suite from attempting to open a browser (for example, during influxctl OAuth2 authentication), and instead execute the host-open script.
RUN echo '#!/bin/bash' > /usr/local/bin/xdg-open \
&& echo 'echo "$1" > /shared/urls.txt' >> /usr/local/bin/xdg-open \
&& echo 'echo "$1" >> /shared/host_open.log' >> /usr/local/bin/xdg-open \
&& chmod +x /usr/local/bin/xdg-open
# Some Python test dependencies (pytest-dotenv and pytest-codeblocks) aren't
# available as packages in apt-cache, so use pip to download dependencies in a # separate step and use Docker's caching.
# Pytest configuration file.
COPY ./test/src/pytest.ini pytest.ini
# Python and Pytest dependencies.
COPY ./test/src/requirements.txt requirements.txt
# Pytest fixtures.
COPY ./test/src/conftest.py conftest.py
RUN pip install -Ur requirements.txt
# Activate the Python virtual environment configured in the Dockerfile.

View File

@ -1,7 +1,6 @@
# Use the Dockerfile 1.2 syntax to leverage BuildKit features like cache mounts and inline mounts--temporary mounts that are only available during the build step, not at runtime.
# syntax=docker/dockerfile:1.2
# Starting from a Go base image is easier than setting up the Go environment later.
FROM python:3.9-slim
# Install the necessary packages for the test environment.

2
test/.gitignore vendored
View File

@ -2,7 +2,9 @@
/Cargo.lock
config.toml
content
monitor_urls_pid
node_modules
shared
tmp
.config*
.env*

View File

@ -1,111 +0,0 @@
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 a `.env.<product-name>` file and add key=value properties--for example, in `.env.serverless`
```text
INFLUX_HOST=https://us-east-1-1.aws.cloud2.influxdata.com
INFLUX_HOSTNAME=us-east-1-1.aws.cloud2.influxdata.com
INFLUX_TOKEN=5Vz...
INFLUX_ORG=28d...
INFLUX_DATABASE=jason-test-create-bucket
INFLUX_RETENTION_POLICY=test-autogen
```
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 `<!--pytest-codeblocks:expected-output-->` comment tag, and then the expected output in a code block--for example:
<!-- Your Markdown content -->
```python
print("Hello, world!")
```
<!--pytest-codeblocks:expected-output-->
If successful, the output is the following:
```
Hello, world!
```
<!-- End Markdown content -->
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.
```

10
test/env.test.example Normal file
View File

@ -0,0 +1,10 @@
ACCOUNT_ID=
CLUSTER_ID=
INFLUX_ORG=
INFLUX_TOKEN=
INFLUX_HOST=https://
INFLUX_HOSTNAME=
INFLUX_DATABASE=myname-test-dedicated
INFLUX_USER_NAME=
INFLUX_RETENTION_POLICY=test-autogen
MANAGEMENT_TOKEN=

60
test/src/conftest.py Normal file
View File

@ -0,0 +1,60 @@
# Test setup fixtures
import pytest
import requests
import os
def v3_management_api():
return {
'url': os.getenv('INFLUX_HOST') + '/api/v0/accounts/' + os.getenv('ACCOUNT_ID') + '/clusters/' + os.getenv('CLUSTER_ID'),
'headers': {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer ' + os.getenv('MANAGEMENT_TOKEN')
}
}
@pytest.fixture
def create_v3_token():
api = v3_management_api()
url = api['url'] + '/databases/' + os.getenv('INFLUX_DATABASE')
headers = api['headers']
data = {
"description": "v3 read token",
"permissions": [
{
"action": "read",
"resource": os.getenv('INFLUX_DATABASE')
},
{
"action": "write",
"resource": os.getenv('INFLUX_DATABASE')
},
]
}
response = requests.post(url, data=data, headers=headers)
return response.json()['token']
# Example test function using the setup_v3_db fixture:
# def test_setup(setup_v3_db):
# database, token = setup_v3_db
# assert database
# assert token
@pytest.fixture
def set_token():
try:
token = create_v3_token()
os.environ.update({'INFLUX_TOKEN': token})
yield token
except Exception as e:
print(e)
assert False
@pytest.fixture
def delete_v3_database():
api = v3_management_api()
url = api['url'] + '/databases/' + os.getenv('INFLUX_TEMP_DATABASE')
headers = api['headers']
response = requests.delete(url, headers=headers)
return response.json()['name']

View File

@ -0,0 +1,23 @@
#!/bin/bash
## This script is meant to be run on the host and monitors a file for URLs written by a container.
# The file to monitor for URLs written by the container.
FILE="./test/shared/urls.txt"
# Define the URL pattern for OAuth2 authorization.
OAUTH_PATTERN='https://auth\.influxdata\.com/activate\?user_code=[A-Z]{1,8}-[A-Z]{1,8}'
# Loop indefinitely
while true; do
if [ -f "$FILE" ]; then
# Extract an OAuth2 authorization URL from the file
URL=$(grep -Eo "$OAUTH_PATTERN" "$FILE")
if [ "$URL" ]; then
# Open the URL in the default browser
open "$URL"
# Clear the file to indicate the URL has been handled
> "$FILE"
fi
fi
sleep 1
done

13
test/src/monitor-tests.sh Executable file
View File

@ -0,0 +1,13 @@
function start {
./test/src/monitor-container-urls.sh & echo $! > ./test/monitor_urls_pid
}
function kill_process {
PID=$(cat ./test/monitor_urls_pid) && kill -9 $PID && rm ./test/monitor_urls_pid
}
case "$1" in
start) start ;;
kill) kill_process ;;
*) echo "Usage: $0 {start|kill}" ;;
esac

View File

@ -20,12 +20,15 @@ function substitute_placeholders {
# Use f-strings to identify placeholders in Python while also keeping valid syntax if
# the user replaces the value.
# Remember to import os for your example code.
sed -i 's/f"DATABASE_TOKEN"/os.getenv("INFLUX_TOKEN")/g;
sed -i 's/f"ACCOUNT_ID"/os.getenv("ACCOUNT_ID")/g;
s/f"API_TOKEN"/os.getenv("INFLUX_TOKEN")/g;
s/f"BUCKET_NAME"/os.getenv("INFLUX_DATABASE")/g;
s/f"CLUSTER_ID"/os.getenv("CLUSTER_ID")/g;
s/f"DATABASE_NAME"/os.getenv("INFLUX_DATABASE")/g;
s/f"DATABASE_TOKEN"/os.getenv("INFLUX_TOKEN")/g;
s/f"get-started"/os.getenv("INFLUX_DATABASE")/g;
s|f"{{< influxdb/host >}}"|os.getenv("INFLUX_HOSTNAME")|g;
s/f"MANAGEMENT_TOKEN"/os.getenv("MANAGEMENT_TOKEN")/g;
s|f"RETENTION_POLICY_NAME\|RETENTION_POLICY"|"autogen"|g;
' $file
@ -35,15 +38,24 @@ function substitute_placeholders {
s|"name": "BUCKET_NAME"|"name": "$INFLUX_DATABASE"|g;' \
$file
sed -i 's/API_TOKEN/$INFLUX_TOKEN/g;
s/ORG_ID/$INFLUX_ORG/g;
s/DATABASE_TOKEN/$INFLUX_TOKEN/g;
s/--bucket-id BUCKET_ID/--bucket-id $INFLUX_BUCKET_ID/g;
s/BUCKET_NAME/$INFLUX_DATABASE/g;
s/DATABASE_NAME/$INFLUX_DATABASE/g;
sed -i 's|"influxctl database create --retention-period 1y get-started"|"influxctl database create --retention-period 1y $INFLUX_TMP_DATABASE"|g;' \
$file
# Replace remaining placeholders with variables.
# If the placeholder is inside of a Python os.getenv() function, don't replace it.
# Note the specific use of double quotes for the os.getenv() arguments here. You'll need to use double quotes in your code samples for this to match.
sed -i '/os.getenv("ACCOUNT_ID")/! s/ACCOUNT_ID/$ACCOUNT_ID/g;
/os.getenv("API_TOKEN")/! s/API_TOKEN/$INFLUX_TOKEN/g;
/os.getenv("BUCKET_ID")/! s/--bucket-id BUCKET_ID/--bucket-id $INFLUX_BUCKET_ID/g;
/os.getenv("BUCKET_NAME")/! s/BUCKET_NAME/$INFLUX_DATABASE/g;
/os.getenv("CLUSTER_ID")/! s/CLUSTER_ID/$CLUSTER_ID/g;
/os.getenv("DATABASE_TOKEN")/! s/DATABASE_TOKEN/$INFLUX_TOKEN/g;
/os.getenv("DATABASE_NAME")/! s/DATABASE_NAME/$INFLUX_DATABASE/g;
s/--id DBRP_ID/--id $INFLUX_DBRP_ID/g;
s/get-started/$INFLUX_DATABASE/g;
s/RETENTION_POLICY_NAME\|RETENTION_POLICY/$INFLUX_RETENTION_POLICY/g;
/os.getenv("MANAGEMENT_TOKEN")/! s/MANAGEMENT_TOKEN/$MANAGEMENT_TOKEN/g;
/os.getenv("ORG_ID")/! s/ORG_ID/$INFLUX_ORG/g;
/os.getenv("RETENTION_POLICY")/! s/RETENTION_POLICY_NAME\|RETENTION_POLICY/$INFLUX_RETENTION_POLICY/g;
s/CONFIG_NAME/CONFIG_$(shuf -i 0-100 -n1)/g;' \
$file

View File

@ -4,6 +4,9 @@ pytest-cov>=2.12.1
pytest-codeblocks>=0.16.1
python-dotenv>=1.0.0
pytest-dotenv>=0.5.2
# Allow pytest to pass if no tests (i.e. testable code blocks) are found.
pytest-custom-exit-code>=0.3.0
requests>=2.26.0
# Code sample dependencies
influxdb3-python @ git+https://github.com/InfluxCommunity/influxdb3-python@v0.5.0
influxdb3-python-cli @ git+https://github.com/InfluxCommunity/influxdb3-python-cli@main