diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 3a8405c..ed6bbc3 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,5 +1,16 @@
name: Test
-on: [push]
+on:
+ push:
+ branches: [main]
+ tags: ["[0-9]+.[0-9]+.[0-9]+"]
+ pull_request: {}
+
+permissions:
+ contents: read
+
+concurrency:
+ group: test-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
release-site-tests:
@@ -14,6 +25,8 @@ jobs:
python-version: '3.13'
- run: python3 -m unittest discover -s tools -p release_site_test.py
- run: python3 -m unittest discover -s tools -p release_notes_test.py
+ - run: python3 -m unittest discover -s tools -p site_artwork_test.py
+ - run: python3 -m unittest discover -s tools -p infrastructure_test.py
- name: Verify configured documentation and generated links
env:
GH_TOKEN: ${{ github.token }}
@@ -47,6 +60,7 @@ jobs:
- uses: actions/checkout@v7
- uses: bazel-contrib/setup-bazel@0.19.0
with:
+ cache-save: ${{ github.ref == 'refs/heads/main' }}
bazelisk-cache: true
# Store build cache per workflow.
disk-cache: ${{ github.workflow }}
@@ -57,6 +71,9 @@ jobs:
bazel test //...
done:
+ permissions:
+ contents: read
+ actions: read
needs: [release-site-tests, pre-commit, test]
if: always()
runs-on: ubuntu-latest
@@ -82,6 +99,25 @@ jobs:
exit 1
fi
echo "done covers all $(grep -c . <<<"${declared}") workflow jobs."
+ - name: Report repository cache usage
+ if: always() && github.ref == 'refs/heads/main'
+ env:
+ GH_TOKEN: ${{ github.token }}
+ GH_REPO: ${{ github.repository }}
+ run: |
+ if gh cache list --limit 10000 --json key,ref,sizeInBytes >cache-inventory.json; then
+ python3 - <<'PYTHON' >>"${GITHUB_STEP_SUMMARY}"
+ import json
+ caches = json.load(open("cache-inventory.json", encoding="utf-8"))
+ size = sum(entry["sizeInBytes"] for entry in caches)
+ print(f"Actions caches: {len(caches)} entries, {size:,} compressed bytes.")
+ print("\nLargest entries:")
+ for entry in sorted(caches, key=lambda item: item["sizeInBytes"], reverse=True)[:10]:
+ print(f"- {entry['key']}: {entry['sizeInBytes']:,} bytes ({entry['ref']})")
+ PYTHON
+ else
+ echo "Cache inventory unavailable." >>"${GITHUB_STEP_SUMMARY}"
+ fi
- name: Fail if any dependency did not succeed
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index 3bd9185..2956d92 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -127,6 +127,7 @@ jobs:
fi
mkdir -p public
rsync --archive --exclude='.git' site/ public/
+ python3 source/tools/site_artwork.py source/docs/assets public
- uses: actions/configure-pages@v6
- uses: actions/upload-pages-artifact@v5
with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 31603d4..78a2c5c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -2,20 +2,24 @@ name: Release
on:
push:
tags:
- - "*.*.*"
+ - "[0-9]+.[0-9]+.[0-9]+"
jobs:
release:
- uses: bazel-contrib/.github/.github/workflows/release_ruleset.yaml@v7
+ uses: bazel-contrib/.github/.github/workflows/release_ruleset.yaml@1d798ff015ed0696433e01e2c3ccbb2abefadad7 # v7.7.0
permissions:
attestations: write
contents: write
id-token: write
with:
release_files: bashtest-*.tar.gz
- # Keep the GitHub release provisional until the secondary BCR
- # publication process has completed.
- prerelease: true
+ # action-gh-release v3 stages standard releases as drafts until all assets
+ # are uploaded, then publishes once. Immutable releases cannot be promoted
+ # from prerelease after publication.
+ prerelease: false
+ # The upstream release workflow cannot restrict saves to main. Keep its
+ # separate release build from populating tag-scoped caches.
+ mount_bazel_caches: false
# Mirror the release to the Bazel Central Registry (replaces the retired
# publish-to-bcr GitHub App). See .github/workflows/publish.yaml.
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7a3b564..c33d73c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -33,6 +33,12 @@ repos:
- repo: local
hooks:
+ - id: infrastructure-tests
+ name: Test publishing and CI policy
+ language: system
+ entry: python3 -m unittest discover -s tools -p '*_test.py'
+ files: ^(tools/|docs/assets/|release-site\.json$|\.github/workflows/)
+ pass_filenames: false
- id: uncomment-bazelmod-includes
name: uncomment-bazelmod-includes
description: |
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..da8e6ce
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,18 @@
+# Agent and contributor rules - bashtest
+
+These rules apply to human and automated contributors. `RULES.md` owns code layout and API
+compatibility; `STYLE_SH.md` owns shell conventions; `GIT_RULES.md` owns branch and PR operations.
+
+Run `bazel test //...`, `python3 -m unittest discover -s tools -p '*_test.py'`, and
+`pre-commit run --all-files` before proposing repository-wide changes. Add regression tests for
+behavior changes. Preserve the supported operating systems in CI and keep direct Bazel dependencies
+explicit. Keep developer-only dependencies separate from the published module where supported.
+
+Never weaken lint rules or required checks to make CI pass. Do not commit Bazel outputs, local rc
+files, caches, or generated release artifacts. Format Starlark with the pinned Buildifier hooks.
+Use the existing shell test framework and its diagnostics. Keep public behavior documented in the
+same change. Keep Markdown tables vertically aligned; generated docs retain their source of truth.
+
+Versions in `MODULE.bazel` and `CHANGELOG.md` must agree. Use `tools/trigger_release.sh` for numeric
+semantic-version release tags. Preserve the existing immutable GitHub release and BCR publication
+flow. [Infrastructure guidance](docs/infrastructure.md) explains CI, caching, and site publishing.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bd943ee..e6a07c6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -4,7 +4,12 @@ All contributions are generally welcome as long as they fit in with the concepts
# Code Rules
-All code must adhere to the [RULES.md](RULES.md) and mostly follows the [Google style](https://google.github.io/styleguide/). Where it diverges, clang-tidy rules are in effect as much as possible.
+All code must adhere to the [RULES.md](RULES.md) and mostly follows the [Google style](https://google.github.io/styleguide/). Where it diverges, the checked-in pre-commit rules are authoritative.
+
+# Workflow and language rules
+
+Follow [AGENTS.md](AGENTS.md), [GIT_RULES.md](GIT_RULES.md), and [STYLE_SH.md](STYLE_SH.md).
+Run `bazel test //...` and keep behavior changes covered by regression tests.
# Run pre-commit
diff --git a/GIT_RULES.md b/GIT_RULES.md
new file mode 100644
index 0000000..aaa1d71
--- /dev/null
+++ b/GIT_RULES.md
@@ -0,0 +1,105 @@
+# Git and pull-request rules
+
+These rules are the source of truth for branch, pull-request, CI, and merge operations in this
+repository. They apply to human and automated contributors. Repository protections are constraints
+to satisfy, never obstacles to bypass.
+
+## State-changing operations
+
+Before pushing, rebasing, retargeting, merging, closing, reopening, or otherwise changing GitHub
+state:
+
+1. Fetch the current base branch and pull-request head.
+2. Read the pull request's current head SHA, base, mergeability, review state, and required checks
+ from GitHub.
+3. Confirm that the operation advances the stated goal and does not invalidate a ready pull request
+ or another change that depends on it.
+4. Identify the exact branch, pull request, or run being changed. Never rely on a stale local or
+ remembered state.
+
+Keep local and remote state distinct. A local branch containing current `main` does not prove that
+the remote pull-request head is current. Push synchronization commits before treating later local
+validation as authoritative.
+
+Do not force-push, rebase, retarget, merge another branch into a pull request, cancel runs, or alter
+a ready pull request speculatively. Such changes are appropriate only to resolve a demonstrated
+failure, conflict, or dependency, and require validation of the resulting head.
+
+## Pull-request descriptions
+
+Every pull-request description has two layers, in this order:
+
+1. A short human-readable explanation of the outcome and why it matters.
+2. An `## AG;DR` section containing the implementation details, reasoning, validation, portability
+ notes, dependencies, and known limitations needed by reviewers or a future contributor.
+
+Update the detailed section whenever a commit changes the implementation or validation. Keep the
+human summary stable unless the outcome or motivation changes.
+
+Descriptions must identify dependencies on other pull requests and any required merge order. Do
+not claim that a check passed unless it ran against the current pushed head in the applicable
+context.
+
+## Review readiness and merging
+
+A pull request is ready to merge only when all of the following are true:
+
+- its intended base and head are current;
+- it is approved and mergeable;
+- all required checks for its current head have completed successfully;
+- its description reflects the complete change;
+- no unresolved dependency requires another pull request to merge first.
+
+When a pull request is ready, merge it without changing its head, base, commits, or branch. Do not
+restart, duplicate, bypass, transplant, or substitute required checks. Checks belong to their exact
+pull-request context even when another commit has the same tree.
+
+Enable auto-merge only after confirming the correct base and head, approval, mergeability, and
+successful required checks. Auto-merge is a convenience, not a substitute for verifying readiness.
+
+## Dependency ordering
+
+Prefer small, independently reviewable pull requests. For a sequential tracked effort, keep one
+pull request as the active merge gate and start the next item from updated `main` only after that
+gate merges.
+
+When changes must be stacked, record both ancestry and semantic dependencies. If a ready base has
+one child, merge the base first; after GitHub retargets the child, require the child's checks to run
+again against its new base. Do not delay a ready base for a child's now-obsolete pre-retarget run.
+
+For multiple open pull requests, inspect more than their declared bases. Account for shared files,
+APIs, build configuration, workflows, generated artifacts, and validation behavior. Merge independent
+roots without invalidating ready work; serialize changes that overlap or alter one another's CI.
+After every merge, push, retarget, or new commit, refresh GitHub state and recompute the order.
+
+## CI and conflict recovery
+
+Treat a failure as evidence to investigate, not a reason to weaken or bypass policy. Inspect the
+failing job and reproduce it locally where practical. Fix the cause or document a narrowly scoped
+exception permitted by repository policy, then rerun validation on the resulting pushed head.
+
+For a conflicted pull request:
+
+1. Verify that it is not already ready to merge.
+2. Refresh its base and identify the exact conflicting changes.
+3. Resolve the conflict on that pull request's branch, preserving compatible behavior.
+4. Run validation appropriate to the combined change and push the resolution.
+5. Update the description and treat all previous checks for the old head as obsolete.
+
+Cancel only runs that are demonstrably obsolete. A slow, queued, or temporarily failing run does
+not by itself justify replacing the head or starting duplicate runs.
+
+## Completion checks
+
+After a pull request merges:
+
+1. Confirm GitHub reports it as merged and record the merge commit.
+2. Verify that `main` contains the intended change.
+3. Inspect the resulting `main` CI and address any regression before declaring the work complete or
+ advancing a dependent change.
+4. Refresh remaining pull requests because bases, conflicts, and required checks may have changed.
+5. Remove or leave remote branches according to repository settings; never delete unmerged work
+ without explicit authorization.
+
+Work is complete only when the intended change is present on `main`, required post-merge automation
+is healthy, and no promised dependent action remains.
diff --git a/README.md b/README.md
index 3b2ebdf..c9bd167 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# bashtest.sh - A Bazel shell test runner.
+# bashtest.sh - A Bazel shell test runner.
-[Release website](https://mboworks.github.io/bashtest/)
+[Release website](https://mboworks.github.io/bashtest/) · [Infrastructure and publishing](docs/infrastructure.md)
This shell test library provides Bazel macro rules to simplify shell testing.
diff --git a/RULES.md b/RULES.md
index a43d319..6163943 100644
--- a/RULES.md
+++ b/RULES.md
@@ -11,3 +11,6 @@ Some rules for the code layout and its development.
* have documentation.
* API changes that are not backwards compatible should not occur in minor version changes.
* Undocumented and private/internal APIs may be changed in any way at any time.
+
+Shared contributor workflow is in [AGENTS.md](AGENTS.md), branch and PR policy in
+[GIT_RULES.md](GIT_RULES.md), and shell conventions in [STYLE_SH.md](STYLE_SH.md).
diff --git a/STYLE_SH.md b/STYLE_SH.md
new file mode 100644
index 0000000..1ddbe81
--- /dev/null
+++ b/STYLE_SH.md
@@ -0,0 +1,62 @@
+# Shell style
+
+Follow the [Google Shell Style Guide](https://google.github.io/styleguide/shellguide.html) unless
+this document or repository tooling says otherwise.
+
+## Tooling
+
+- Use Bash. Executable scripts start with `#!/usr/bin/env bash`, followed by the licence header.
+- Format with the pinned `beautysh` pre-commit hook.
+- Run ShellCheck and explain necessary local suppressions next to the affected code.
+- Use `set -euo pipefail` in scripts unless a documented compatibility constraint prevents it.
+- Quote expansions unless word splitting is intentional and documented.
+- Make function variables `local` and use lower-case names for locals and functions.
+
+The formatter and linter versions are pinned in [`.pre-commit-config.yaml`](.pre-commit-config.yaml).
+
+## Functions and state
+
+- A function returns data on standard output and diagnostics on standard error.
+- Do not return results by mutating caller variables, global counters, the working directory, shell
+ options, or traps.
+- A function whose stated purpose is an external effect may perform that effect; keep its scope
+ narrow and explicit.
+- Avoid `eval`, `printf -v`, and string-built command lines. Use arrays for commands and arguments.
+- Prefer small functions with one clear responsibility.
+
+```sh
+make_tree() {
+ local root
+ root="$(mktemp -d)"
+ mkdir -p "${root}/src"
+ echo "${root}"
+}
+
+tree="$(make_tree)"
+```
+
+## Conditions and loops
+
+- Prefer `[[ ... ]]` for string and file tests and `(( ... ))` for arithmetic.
+- Use `case` for multi-way string matching.
+- Read lines with `while IFS= read -r line`; do not lose backslashes or surrounding whitespace.
+- Do not parse structured data with fragile `grep`/`sed` pipelines when `jq`, `yq`, or a small
+ checked-in Python tool provides a reliable parser.
+
+## Temporary files and cleanup
+
+- Tests put temporary files below `${BASHTEST_TMPDIR}` and use `test_tmpdir name` for retained fixtures.
+ The framework owns their cleanup; test bodies must not add cleanup traps or recursive deletion.
+- Quote and validate deletion targets. Never recursively delete an unresolved variable, a broad
+ workspace path, or a user directory.
+- Framework/standalone scripts may use a cleanup trap only for resources they own, after the path is known.
+- Bazel tests use `${TEST_TMPDIR}` for target-owned temporary output and resolve runfiles through
+ `${TEST_SRCDIR}` and `${TEST_WORKSPACE}`.
+
+## Portability
+
+- Support the Bash versions available on the macOS and Linux runners in the CI matrix.
+- Do not assume GNU-only flags in scripts that run on macOS; branch on capabilities when necessary.
+- Avoid `mapfile` where a script must run under the system Bash shipped with macOS.
+- Preserve upper-case names for environment and Bazel runfile variables.
+- Check required external programs early and fail with a useful message.
diff --git a/docs/assets/apple-touch-icon.png b/docs/assets/apple-touch-icon.png
new file mode 100644
index 0000000..fd78e1c
Binary files /dev/null and b/docs/assets/apple-touch-icon.png differ
diff --git a/docs/assets/favicon.ico b/docs/assets/favicon.ico
new file mode 100644
index 0000000..4ebcd44
Binary files /dev/null and b/docs/assets/favicon.ico differ
diff --git a/docs/assets/favicon.png b/docs/assets/favicon.png
new file mode 100644
index 0000000..db79b35
Binary files /dev/null and b/docs/assets/favicon.png differ
diff --git a/docs/assets/mboworks-logo.png b/docs/assets/mboworks-logo.png
new file mode 100644
index 0000000..40aa155
Binary files /dev/null and b/docs/assets/mboworks-logo.png differ
diff --git a/docs/infrastructure.md b/docs/infrastructure.md
new file mode 100644
index 0000000..0953b4e
--- /dev/null
+++ b/docs/infrastructure.md
@@ -0,0 +1,50 @@
+# Infrastructure and publishing
+
+Adapted from [proto PR 100](https://github.com/mboworks/proto/pull/100) and xff PRs 835–848,
+excluding 841. This repository keeps its Starlark/shell test pipeline and native setup-bazel
+cache support. C++ clang-tidy orchestration, sanitizer caches, and coverage history ordering do
+not apply to its current jobs.
+
+## CI and caching
+
+Main and numeric release tags run push validation; branches run in pull-request context,
+including forks, without duplicating the complete matrix on each push. Only superseded PR runs
+are cancelled. The final gate retains release-site tests, pre-commit, and every test-matrix cell.
+
+The pinned setup-bazel action retains native platform-specific cache paths, including Windows
+where supported. In the Test workflow, PRs and release tags restore caches; only main saves them.
+The separate upstream Release build disables cache mounting because it cannot restrict writes
+to main. Bazelisk and
+repository downloads remain cached because these small projects do not download the C++ LLVM
+payload that motivated compiled-output-only caching in proto and xff. The action's existing
+OS/architecture and build/dependency-hash keys remain intact. Exact cache hits are immutable;
+source-only edits need not refresh these small build-configuration caches.
+
+The final main job reports compressed repository cache usage and the ten largest entries.
+Use these measurements before introducing stricter budgets or custom eviction. A missing
+inventory is reported without replacing test failures. This inventory includes all refs and
+may briefly lag post-job cache uploads; verify later runs before claiming savings.
+
+## Publishing
+
+The release publisher uses trusted main tooling and the tagged documentation snapshot.
+The shared 64-pixel README logo and favicons match MBO Works' other repositories.
+Favicons are inserted after staging the complete deployment copy; retained release snapshots
+stay unchanged. Decoration is idempotent and resolves links from nested pages.
+
+The shared release workflow is pinned to the same v7.7.0 commit as proto. Standard releases
+are staged as drafts until archives and attestations are uploaded, then published once. Published
+immutable prereleases cannot later be promoted. BCR publication follows release success and can
+be retried independently; its failure does not require recreating a release or moving its tag.
+Release helpers, numeric tags, and version agreement keep their existing contracts. No module
+dependency versions change in this rollout.
+
+## Contributor rules and verification
+
+`AGENTS.md`, `GIT_RULES.md`, and `STYLE_SH.md` synchronize applicable shared rules. `RULES.md`
+retains this repository's layout and API promises. Beautysh remains the shell formatter; no C++
+style guide or competing shell formatter is introduced.
+
+Run `bazel test //...`, `python3 -m unittest discover -s tools -p '*_test.py'`, and
+`pre-commit run --all-files`. CI also builds the configured release documentation to validate
+links and mappings before publication.
diff --git a/release-site.json b/release-site.json
index db569c0..109558b 100644
--- a/release-site.json
+++ b/release-site.json
@@ -6,7 +6,11 @@
"bashtest/README.md": "bashtest/README.html",
"CODE_OF_CONDUCT.md": "CODE_OF_CONDUCT.html",
"RULES.md": "RULES.html",
- "docs/release-website.md": "docs/release-website.html"
+ "docs/release-website.md": "docs/release-website.html",
+ "AGENTS.md": "AGENTS.html",
+ "GIT_RULES.md": "GIT_RULES.html",
+ "STYLE_SH.md": "STYLE_SH.html",
+ "docs/infrastructure.md": "docs/infrastructure.html"
},
"links": []
}
diff --git a/tools/infrastructure_test.py b/tools/infrastructure_test.py
new file mode 100644
index 0000000..cfcb4be
--- /dev/null
+++ b/tools/infrastructure_test.py
@@ -0,0 +1,38 @@
+# SPDX-FileCopyrightText: Copyright (c) M. Boerger, the MBO Works authors
+# SPDX-License-Identifier: Apache-2.0
+"""Regression checks for PR cache isolation and complete validation gates."""
+
+from pathlib import Path
+import unittest
+
+
+class InfrastructureTest(unittest.TestCase):
+ def test_prs_restore_but_only_main_writes_caches(self):
+ workflow = (Path(__file__).parents[1] / '.github/workflows/main.yml').read_text()
+ self.assertIn('pull_request: {}', workflow)
+ self.assertIn("cache-save: ${{ github.ref == 'refs/heads/main' }}", workflow)
+ self.assertIn("cancel-in-progress: ${{ github.event_name == 'pull_request' }}", workflow)
+ self.assertIn('repository-cache: true', workflow)
+ self.assertIn('disk-cache: ${{ github.workflow }}', workflow)
+ self.assertIn('needs: [release-site-tests, pre-commit, test]', workflow)
+ self.assertIn('os: [ubuntu-latest, macos-latest]', workflow)
+
+ def test_release_uploads_before_immutable_publication(self):
+ workflow = (Path(__file__).parents[1] / '.github/workflows/release.yml').read_text()
+ self.assertIn('release_ruleset.yaml@1d798ff015ed0696433e01e2c3ccbb2abefadad7', workflow)
+ self.assertIn('prerelease: false', workflow)
+ self.assertIn('mount_bazel_caches: false', workflow)
+ self.assertIn('needs: release', workflow)
+ self.assertNotIn('prerelease: true', workflow)
+
+ def test_documented_rules_are_published(self):
+ import json
+ root = Path(__file__).parents[1]
+ config = json.loads((root / 'release-site.json').read_text())
+ for name in ('AGENTS.md', 'GIT_RULES.md', 'STYLE_SH.md', 'docs/infrastructure.md'):
+ self.assertTrue((root / name).is_file())
+ self.assertIn(name, config['pages'])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/site_artwork.py b/tools/site_artwork.py
new file mode 100644
index 0000000..0ef9e89
--- /dev/null
+++ b/tools/site_artwork.py
@@ -0,0 +1,47 @@
+# SPDX-FileCopyrightText: Copyright (c) M. Boerger, the MBO Works authors
+# SPDX-License-Identifier: Apache-2.0
+"""Add shared favicons to the staged Pages tree, leaving retained releases unchanged."""
+
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+
+
+ICONS = ("favicon.ico", "favicon.png", "apple-touch-icon.png")
+MARKER = ''
+
+
+def decorate(assets: Path, output: Path) -> None:
+ for name in ICONS:
+ shutil.copyfile(assets / name, output / name)
+ for path in output.rglob("*.html"):
+ text = path.read_text(encoding="utf-8")
+ if MARKER in text:
+ continue
+ opening = re.search(r"
A fragment
', + } + for name, text in documents.items(): + path = root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text) + site_artwork.decorate(assets, root) + for name, original in documents.items(): + path = root / name + decorated = path.read_text() + if name == "fragment.html": + self.assertEqual(decorated, original) + continue + prefix = "." if name == "index.html" else "../../.." + for icon in site_artwork.ICONS: + self.assertIn(f'href="{prefix}/{icon}"', decorated) + self.assertEqual((path.parent / prefix / icon).read_bytes(), + (assets / icon).read_bytes()) + self.assertIn('sizes="32x32"', decorated) + self.assertIn('sizes="180x180"', decorated) + self.assertEqual(decorated.count(site_artwork.MARKER), 1) + before = {name: (root / name).read_bytes() for name in documents} + site_artwork.decorate(assets, root) + self.assertEqual(before, {name: (root / name).read_bytes() for name in documents}) + + +if __name__ == "__main__": + unittest.main()