diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7da7c2f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,85 @@ +name: CI + +on: + pull_request: + branches: + - main + +jobs: + branch-naming: + name: Validate branch name + runs-on: ubuntu-latest + steps: + - name: Check branch name format + run: | + BRANCH="${{ github.head_ref }}" + echo "Checking branch name: $BRANCH" + + # Must start with a valid prefix + if [[ ! "$BRANCH" =~ ^(feature|bugfix|hotfix|chore|docs|refactor|test)/ ]]; then + echo "::error::Branch name must start with a valid prefix (feature/, bugfix/, hotfix/, chore/, docs/, refactor/, test/)" + exit 1 + fi + + # After prefix, must be lowercase kebab-case + SUFFIX="${BRANCH#*/}" + if [[ ! "$SUFFIX" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then + echo "::error::Branch name after prefix must be lowercase kebab-case (e.g., feature/my-new-feature)" + exit 1 + fi + + echo "Branch name is valid." + + format: + name: Check formatting + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: true + + - name: Setup Rokit + uses: CompeyDev/setup-rokit@v0.1.2 + + - name: Check formatting + run: lune run ./Submodules/luau-cicd/Scripts/CheckFormatting.luau + + test: + name: Run tests + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: true + + - name: Setup Rokit + uses: CompeyDev/setup-rokit@v0.1.2 + + - name: Install dependencies + run: wally install + + - name: Run tests + run: lune run ./Scripts/RunTests.luau + + analyze: + name: Static analysis + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: true + + - name: Setup Rokit + uses: CompeyDev/setup-rokit@v0.1.2 + + - name: Install dependencies + run: wally install + + - name: Setup Lune typedefs + run: lune setup --no-update-luaurc + + - name: Run static analysis + run: luau-lsp analyze --ignore "Source/Testable/init.luau" --ignore "Submodules/**" --platform standard . diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml deleted file mode 100644 index 7db441a..0000000 --- a/.github/workflows/format.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Format - -on: - push: - branches: - - main - -jobs: - format: - name: Check formatting - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Rokit - uses: CompeyDev/setup-rokit@v0.1.2 - - - name: Check formatting - run: lune run ./Scripts/CheckFormatting.luau diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8c548ff..bf4ceab 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,6 +15,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + submodules: true - name: Setup Rokit uses: CompeyDev/setup-rokit@v0.1.2 diff --git a/.github/workflows/release-checks.yml b/.github/workflows/release-checks.yml index 78ac33b..f51eaad 100644 --- a/.github/workflows/release-checks.yml +++ b/.github/workflows/release-checks.yml @@ -6,21 +6,48 @@ on: - release jobs: - test: - name: Run tests + pr-title: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - name: Check PR title format + run: | + TITLE="${{ github.event.pull_request.title }}" + echo "Checking PR title: $TITLE" + + # Must be exactly "Release X.Y.Z" + if [[ ! "$TITLE" =~ ^Release\ [0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::PR title must be exactly 'Release X.Y.Z' (e.g., 'Release 1.2.3')" + exit 1 + fi + + echo "PR title is valid." + + diff-check: + name: Verify diff matches main runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true - - name: Setup Rokit - uses: CompeyDev/setup-rokit@v0.1.2 + - name: Check diff with main + run: | + git fetch origin main - - name: Install dependencies - run: wally install + # Get the diff between the PR branch and main + DIFF=$(git diff origin/main..HEAD) - - name: Run tests - run: lune run ./Scripts/RunTests.luau + if [ -n "$DIFF" ]; then + echo "::error::PR branch has changes that differ from main. The release branch must contain exactly what is in main." + echo "Diff:" + echo "$DIFF" + exit 1 + fi + + echo "PR branch matches main exactly." format: name: Check formatting @@ -28,12 +55,32 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + submodules: true - name: Setup Rokit uses: CompeyDev/setup-rokit@v0.1.2 - name: Check formatting - run: lune run ./Scripts/CheckFormatting.luau + run: lune run ./Submodules/luau-cicd/Scripts/CheckFormatting.luau + + test: + name: Run tests + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: true + + - name: Setup Rokit + uses: CompeyDev/setup-rokit@v0.1.2 + + - name: Install dependencies + run: wally install + + - name: Run tests + run: lune run ./Scripts/RunTests.luau analyze: name: Static analysis @@ -41,15 +88,20 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + submodules: true - name: Setup Rokit uses: CompeyDev/setup-rokit@v0.1.2 + - name: Install dependencies + run: wally install + - name: Setup Lune typedefs run: lune setup --no-update-luaurc - name: Run static analysis - run: luau-lsp analyze --ignore "Source/Testable/init.luau" --platform standard . + run: luau-lsp analyze --ignore "Source/Testable/init.luau" --ignore "Submodules/**" --platform standard . version: name: Validate version @@ -59,15 +111,29 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + submodules: true - name: Setup Rokit uses: CompeyDev/setup-rokit@v0.1.2 - name: Check version update - run: lune run ./Scripts/EnsureProperVersionUpdate.luau + run: lune run ./Submodules/luau-cicd/Scripts/EnsureProperVersionUpdate.luau - name: Check version match - run: lune run ./Scripts/CheckVersionMatch.luau + run: lune run ./Submodules/luau-cicd/Scripts/CheckVersionMatch.luau - name: Check changelog entry - run: lune run ./Scripts/CheckChangelogVersion.luau + run: lune run ./Submodules/luau-cicd/Scripts/CheckChangelogVersion.luau + + - name: Verify PR title matches VERSION + run: | + VERSION=$(cat VERSION | tr -d '[:space:]') + EXPECTED_TITLE="Release $VERSION" + ACTUAL_TITLE="${{ github.event.pull_request.title }}" + + if [ "$EXPECTED_TITLE" != "$ACTUAL_TITLE" ]; then + echo "::error::PR title '$ACTUAL_TITLE' does not match VERSION file. Expected '$EXPECTED_TITLE'" + exit 1 + fi + + echo "PR title matches VERSION file." diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index c9d8cac..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Test - -on: - push: - branches: - - main - -jobs: - test: - name: Run tests - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Rokit - uses: CompeyDev/setup-rokit@v0.1.2 - - - name: Install dependencies - run: wally install - - - name: Run tests - run: lune run ./Scripts/RunTests.luau diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0d4538a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "Submodules/claude-md-luau"] + path = Submodules/claude-md-luau + url = git@github.com:horsenuggets/claude-md-luau.git +[submodule "Submodules/luau-cicd"] + path = Submodules/luau-cicd + url = git@github.com:horsenuggets/luau-cicd.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 41ce70d..eb6ca6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.0 + +### Changed + +- Use luau-cicd submodule for CI/CD scripts instead of local copies + ## 0.0.5 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 50b20f2..5584df7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,114 +1,3 @@ # Claude Code Guidelines -## Commits - -- Always break commits down into logical parts -- Do not co-author yourself in commits - -## Formatting - -- Run `stylua .` often to ensure that code is formatted properly -- Every file should end in a single newline -- Text should be LF normalized -- Prefer Luau string interpolation using backticks, like `` `Here is a string with an interpolated {value}.` `` -- Prefer double quotes over single quotes -- Always read through existing code to match style - -## Luau File Headers - -Every Luau file should have this at the top: - -```luau ---[[ - - - - - ---]] -``` - -For `init.luau` files, use the parent folder name instead of "init". - -## Comments - -- All comments should word-wrap at column 90 - -## Functions - -- Always add runtime typechecking to function parameters using assert - -## Operators - -- Use compound assignment operators (`+=`, `-=`, `*=`, `/=`) instead of expanded form - -## Print Statements - -- Avoid using colons `:` in prints for stylistic reasons -- Structure everything in complete sentences -- Surround strings of interest in quotation marks `"` -- Use `[Usage]` instead of `Usage:` for usage messages - -## Versioning - -- Version tags should NOT have a "v" prefix (use `0.0.1`, not `v0.0.1`) - -## Changelog Format - -CHANGELOG.md should follow this format: - -```md -# Changelog - -## 0.0.2 - -### Added - -- This is an example addition -- This is another example addition - -### Changed - -- This is an example change -- This is another example change - -### Fixed - -- This is an example fix -- This is another example fix - -## 0.0.1 - -### Added - -- This is an example addition -- This is another example addition - -### Changed - -- This is an example change -- This is another example change - -### Fixed - -- This is an example fix -- This is another example fix -``` - -## Ordering - -- When things can be sorted alphabetically, definitely do that (e.g., imports, table keys, function parameters) - -## Tests - -- For TestEZ-style tests, do not wrap everything in a describe block with just the file name -- The file name is already used as the test name, so a wrapping describe block is redundant - -## Lune Documentation - -You can read Lune documentation as needed to understand the Lune code you're writing: - -- https://lune-org.github.io/docs/api-reference/fs -- https://lune-org.github.io/docs/api-reference/net -- https://lune-org.github.io/docs/api-reference/process -- https://lune-org.github.io/docs/api-reference/* +Detailed guidelines for this project can be found at [`Submodules/claude-md-luau/CLAUDE.md`](Submodules/claude-md-luau/CLAUDE.md). diff --git a/LICENSE b/LICENSE index f21b91f..66b6234 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 HorseNuggets +Copyright (c) 2026 HorseNuggets 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 7671492..590c209 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Testable +# testable A Luau testing framework based off of TestEZ. Testable extends TestEZ with parallel test execution and support for [Lune](https://lune-org.github.io/docs), allowing you to run tests outside of the Roblox environment. diff --git a/Scripts/BumpVersion.luau b/Scripts/BumpVersion.luau deleted file mode 100644 index 941c7ff..0000000 --- a/Scripts/BumpVersion.luau +++ /dev/null @@ -1,68 +0,0 @@ ---[[ - -BumpVersion - -Bumps the version in VERSION file and syncs it to wally.toml. Pass "major", "minor", or -"patch" as an argument to bump the corresponding version component. - -[Usage] lune run Scripts/BumpVersion - ---]] - -local fs = require("@lune/fs") -local process = require("@lune/process") - -local parseVersion = require("./Helpers/parseVersion") - -local function bumpVersion() - local args = process.args - if #args < 1 then - print("[Usage] lune run Scripts/BumpVersion ") - process.exit(1) - end - - local bumpType = string.lower(args[1]) - if bumpType ~= "major" and bumpType ~= "minor" and bumpType ~= "patch" then - print(`Invalid bump type "{bumpType}". Must be "major", "minor", or "patch".`) - process.exit(1) - end - - local currentVersionString = fs.readFile("VERSION") - local major, minor, patch = parseVersion(currentVersionString) - - if not major then - print(`Could not parse current version "{currentVersionString}".`) - process.exit(1) - end - - local previousVersion = `{major}.{minor}.{patch}` - print(`Current version is "{previousVersion}".`) - - if bumpType == "major" then - major += 1 - minor = 0 - patch = 0 - elseif bumpType == "minor" then - minor += 1 - patch = 0 - else - patch += 1 - end - - local newVersion = `{major}.{minor}.{patch}` - print(`Bumping to "{newVersion}".`) - - fs.writeFile("VERSION", newVersion .. "\n") - - print("Syncing version to wally.toml.") - local syncResult = process.exec("lune", { "run", "Scripts/SyncVersion" }) - if not syncResult.ok then - print("Failed to sync version.") - print(syncResult.stderr) - process.exit(1) - end - - print("Done.") -end - -bumpVersion() diff --git a/Scripts/CheckChangelogVersion.luau b/Scripts/CheckChangelogVersion.luau deleted file mode 100644 index f0834b9..0000000 --- a/Scripts/CheckChangelogVersion.luau +++ /dev/null @@ -1,59 +0,0 @@ ---[[ - -CheckChangelogVersion - -Validates that CHANGELOG.md contains an entry for the current VERSION. - ---]] - -local fs = require("@lune/fs") -local process = require("@lune/process") - -local function checkChangelogVersion() - -- Read current version - if not fs.isFile("VERSION") then - print("VERSION file not found.") - process.exit(1) - end - - local version = fs.readFile("VERSION") - version = string.match(version, "^%s*(.-)%s*$") - - print(`Checking for version "{version}" in CHANGELOG.md...`) - - -- Read changelog - if not fs.isFile("CHANGELOG.md") then - print("CHANGELOG.md not found.") - process.exit(1) - end - - local changelog = fs.readFile("CHANGELOG.md") - - -- Check if version header exists - local pattern = "## " .. version:gsub("%.", "%%.") - if not string.find(changelog, pattern) then - print(`CHANGELOG.md does not contain an entry for version "{version}".`) - print("Please add a changelog section before releasing.") - process.exit(1) - end - - -- Check that the version section has content - local sectionPattern = "## " .. version:gsub("%.", "%%.") .. "\n(.-)\n## " - local section = string.match(changelog, sectionPattern) - - if not section then - -- Try matching to end of file - sectionPattern = "## " .. version:gsub("%.", "%%.") .. "\n(.-)$" - section = string.match(changelog, sectionPattern) - end - - if not section or string.match(section, "^%s*$") then - print(`CHANGELOG.md entry for version "{version}" is empty.`) - print("Please add changelog content before releasing.") - process.exit(1) - end - - print(`CHANGELOG.md contains valid entry for version "{version}".`) -end - -checkChangelogVersion() diff --git a/Scripts/CheckFormatting.luau b/Scripts/CheckFormatting.luau deleted file mode 100644 index 6485b29..0000000 --- a/Scripts/CheckFormatting.luau +++ /dev/null @@ -1,25 +0,0 @@ ---[[ - -CheckFormatting - -Verifies that all code is properly formatted using stylua. - ---]] - -local process = require("@lune/process") - -local function checkFormatting() - local result = process.exec("stylua", { "--check", "." }) - - if result.ok then - print("Formatting check passed!") - process.exit(0) - else - print("Formatting check failed.") - print(result.stdout) - print(result.stderr) - process.exit(1) - end -end - -checkFormatting() diff --git a/Scripts/CheckVersionMatch.luau b/Scripts/CheckVersionMatch.luau deleted file mode 100644 index 8b749cf..0000000 --- a/Scripts/CheckVersionMatch.luau +++ /dev/null @@ -1,34 +0,0 @@ ---[[ - -CheckVersionMatch - -Ensures that the version in VERSION file matches the version in wally.toml. Exits with -code 0 if they match, 1 if they do not. - ---]] - -local fs = require("@lune/fs") -local process = require("@lune/process") - -local function checkVersionMatch() - local versionFileContent = fs.readFile("VERSION") - local version = string.match(versionFileContent, "^%s*(.-)%s*$") - - local wallyContent = fs.readFile("wally.toml") - local wallyVersion = string.match(wallyContent, '%[package%][^%[]*version%s*=%s*"(.-)"') - - if not wallyVersion then - print("Could not find version in wally.toml.") - process.exit(1) - end - - if version == wallyVersion then - print(`Versions match "{version}".`) - process.exit(0) - else - print(`Version mismatch. VERSION file has "{version}" but wally.toml has "{wallyVersion}".`) - process.exit(1) - end -end - -checkVersionMatch() diff --git a/Scripts/EnsureProperVersionUpdate.luau b/Scripts/EnsureProperVersionUpdate.luau deleted file mode 100644 index 5648d92..0000000 --- a/Scripts/EnsureProperVersionUpdate.luau +++ /dev/null @@ -1,120 +0,0 @@ ---[[ - -EnsureProperVersionUpdate - -Validates that the VERSION file contains a proper semantic version and that it has been -bumped correctly since the last release tag. Compares against the most recent git tag -(excluding 0.0.0) to determine if a valid version bump has occurred. - ---]] - -local fs = require("@lune/fs") -local process = require("@lune/process") - -local parseVersion = require("./Helpers/parseVersion") - -local function ensureProperVersionUpdate() - assert(fs.isFile("VERSION"), "VERSION file does not exist.") - - -- Read current VERSION file - local currentVersionString = fs.readFile("VERSION") - local currentMajor, currentMinor, currentPatch = parseVersion(currentVersionString) - - -- Validate current version format - if not currentMajor then - print( - `VERSION file is not in MAJOR.MINOR.PATCH format, current content is "{string.match( - currentVersionString, - "^%s*(.-)%s*$" - )}".` - ) - process.exit(1) - end - - print(`Current version is "{currentMajor}.{currentMinor}.{currentPatch}".`) - - -- Get the latest tag (excluding v0.0.0) - local tagResult = process.exec("git", { "tag", "--sort=-version:refname" }) - - if not tagResult.ok then - print("Could not retrieve git tags.") - print("Version format is valid.") - return - end - - -- Find the first tag that isn't 0.0.0 - local latestTag = nil - for tag in string.gmatch(tagResult.stdout, "[^\n]+") do - if tag ~= "0.0.0" then - latestTag = tag - break - end - end - - if not latestTag then - print("No previous release tags found.") - print("Version format is valid.") - return - end - - print(`Latest release tag is "{latestTag}".`) - - -- Get VERSION from the tag - local versionAtTagResult = process.exec("git", { "show", `{latestTag}:VERSION` }) - - if not versionAtTagResult.ok then - print(`Could not read VERSION at tag "{latestTag}".`) - print("Version format is valid.") - return - end - - local previousVersionString = versionAtTagResult.stdout - local previousMajor, previousMinor, previousPatch = parseVersion(previousVersionString) - - if not previousMajor then - print(`VERSION at tag "{latestTag}" was not in proper format, skipping comparison.`) - print("Current version format is valid.") - return - end - - print(`Version at last release was "{previousMajor}.{previousMinor}.{previousPatch}".`) - - -- Check if versions are identical - if currentMajor == previousMajor and currentMinor == previousMinor and currentPatch == previousPatch then - print("Version has not been updated since last release.") - process.exit(1) - end - - -- Validate proper semantic version bump - -- Only one of these should be true: - -- 1. Major bump: major+1, minor=0, patch=0 - -- 2. Minor bump: major same, minor+1, patch=0 - -- 3. Patch bump: major same, minor same, patch+1 - - local isMajorBump = currentMajor == previousMajor + 1 and currentMinor == 0 and currentPatch == 0 - local isMinorBump = currentMajor == previousMajor and currentMinor == previousMinor + 1 and currentPatch == 0 - local isPatchBump = currentMajor == previousMajor - and currentMinor == previousMinor - and currentPatch == previousPatch + 1 - - if isMajorBump then - print("Valid major version bump.") - elseif isMinorBump then - print("Valid minor version bump.") - elseif isPatchBump then - print("Valid patch version bump.") - else - print("Invalid version bump, must follow semantic versioning.") - print("Valid bumps are") - print(` Major: {previousMajor}.{previousMinor}.{previousPatch} -> {previousMajor + 1}.0.0`) - print(` Minor: {previousMajor}.{previousMinor}.{previousPatch} -> {previousMajor}.{previousMinor + 1}.0`) - print( - ` Patch: {previousMajor}.{previousMinor}.{previousPatch} -> {previousMajor}.{previousMinor}.{previousPatch + 1}` - ) - process.exit(1) - end - - print("Version update is valid.") -end - -ensureProperVersionUpdate() diff --git a/Scripts/Helpers/extractChangelog.luau b/Scripts/Helpers/extractChangelog.luau deleted file mode 100644 index 8e9fc7e..0000000 --- a/Scripts/Helpers/extractChangelog.luau +++ /dev/null @@ -1,40 +0,0 @@ ---[[ - -extractChangelog - -Extracts the changelog section for a specific version from CHANGELOG.md. - ---]] - -local fs = require("@lune/fs") - -local function extractChangelog(version: string): string? - assert(type(version) == "string", "version must be a string") - - if not fs.isFile("CHANGELOG.md") then - return nil - end - - local content = fs.readFile("CHANGELOG.md") - - -- Pattern to match version header and content until next version header or end - local pattern = "## " .. version:gsub("%.", "%%.") .. "\n(.-)\n## " - local section = string.match(content, pattern) - - if not section then - -- Try matching to end of file (for last version in changelog) - pattern = "## " .. version:gsub("%.", "%%.") .. "\n(.-)$" - section = string.match(content, pattern) - end - - if not section then - return nil - end - - -- Trim leading and trailing whitespace - section = string.match(section, "^%s*(.-)%s*$") - - return section -end - -return extractChangelog diff --git a/Scripts/Helpers/parseVersion.luau b/Scripts/Helpers/parseVersion.luau deleted file mode 100644 index a53bade..0000000 --- a/Scripts/Helpers/parseVersion.luau +++ /dev/null @@ -1,37 +0,0 @@ ---[[ - -parseVersion - -Parses a semantic version string in MAJOR.MINOR.PATCH format and returns the three -components as numbers. Returns nil for all three values if the string is not valid. -Version numbers must be between 0 and 99999. - ---]] - -local MAX_VERSION_NUMBER = 99999 - -local function parseVersion(inputVersionString: string): (number?, number?, number?) - assert(typeof(inputVersionString) == "string", "Expected a string.") - - -- Trim whitespace - local versionString: string = string.match(inputVersionString, "^%s*(.-)%s*$") or "" - - -- Parse MAJOR.MINOR.PATCH format - local major, minor, patch = string.match(versionString, "^(%d+)%.(%d+)%.(%d+)$") - - if not major or not minor or not patch then - return nil, nil, nil - end - - local majorNum, minorNum, patchNum = tonumber(major), tonumber(minor), tonumber(patch) - - -- Validate version numbers are within reasonable bounds - if majorNum > MAX_VERSION_NUMBER or minorNum > MAX_VERSION_NUMBER or patchNum > MAX_VERSION_NUMBER then - print(`Version number exceeds maximum allowed value of {MAX_VERSION_NUMBER}.`) - return nil, nil, nil - end - - return majorNum, minorNum, patchNum -end - -return parseVersion diff --git a/Scripts/SyncVersion.luau b/Scripts/SyncVersion.luau deleted file mode 100644 index eeac55d..0000000 --- a/Scripts/SyncVersion.luau +++ /dev/null @@ -1,33 +0,0 @@ ---[[ - -SyncVersion - -Synchronizes the version from the VERSION file to wally.toml. - ---]] - -local fs = require("@lune/fs") - -local function syncVersion() - -- Read version from VERSION file - local version = fs.readFile("VERSION") - - -- Trim whitespace from version - version = string.match(version, `^%s*(.-)%s*$`) - - -- Read wally.toml - local wallyContent = fs.readFile("wally.toml") - - -- Find and replace version in [package] section only - -- Pattern matches [package] section, then finds the first version = "..." line - local updatedContent = string.gsub(wallyContent, `(%[package%][^%[]*version%s*=%s*)"(.-)"`, function(prefix, _) - return `{prefix}"{version}"` - end, 1) - - -- Write updated content back to wally.toml - fs.writeFile("wally.toml", updatedContent) - - print(`Updated wally.toml version to "{version}"!`) -end - -syncVersion() diff --git a/Submodules/claude-md-luau b/Submodules/claude-md-luau new file mode 160000 index 0000000..bf77882 --- /dev/null +++ b/Submodules/claude-md-luau @@ -0,0 +1 @@ +Subproject commit bf77882b9ad3af50f9130a5fed464c52f6036ef2 diff --git a/Submodules/luau-cicd b/Submodules/luau-cicd new file mode 160000 index 0000000..96b009c --- /dev/null +++ b/Submodules/luau-cicd @@ -0,0 +1 @@ +Subproject commit 96b009c4891e48927d073e97475b73c91a1bfefa diff --git a/Tests/ConfigTest.spec.luau b/Tests/ConfigTest.spec.luau index e3a5557..c254dfb 100644 --- a/Tests/ConfigTest.spec.luau +++ b/Tests/ConfigTest.spec.luau @@ -1,6 +1,6 @@ --[[ -ConfigTest +ConfigTest.spec Tests for the Testable configuration system including setting and resetting options. diff --git a/Tests/ExampleTest.spec.luau b/Tests/ExampleTest.spec.luau index b5fb46f..6c3a6f9 100644 --- a/Tests/ExampleTest.spec.luau +++ b/Tests/ExampleTest.spec.luau @@ -1,6 +1,6 @@ --[[ -ExampleTest +ExampleTest.spec A simple example test demonstrating basic Testable usage. diff --git a/Tests/ExpectationTest.spec.luau b/Tests/ExpectationTest.spec.luau index a8830f1..d214454 100644 --- a/Tests/ExpectationTest.spec.luau +++ b/Tests/ExpectationTest.spec.luau @@ -1,6 +1,6 @@ --[[ -ExpectationTest +ExpectationTest.spec Tests for all expectation matchers in the Testable framework, including equality checks, type assertions, nil checks, numeric comparisons, error throwing, and negation. diff --git a/Tests/FailTest.spec.luau b/Tests/FailTest.spec.luau index 91e3490..581e6c4 100644 --- a/Tests/FailTest.spec.luau +++ b/Tests/FailTest.spec.luau @@ -1,19 +1,175 @@ --[[ -FailTest +FailTest.spec -Tests for the fail() function in the Testable framework. +Tests for the fail() function in the Testable framework. Uses subprocess execution to +verify that fail() actually causes tests to fail with the expected exit codes. --]] +local fs = require("@lune/fs") +local process = require("@lune/process") + +local testCounter = 0 + +local function createTempScript(): string + testCounter += 1 + local timestamp = os.time() + local random = math.random(100000, 999999) + local scriptName = `_temp_fail_test_{timestamp}_{testCounter}_{random}.luau` + return scriptName +end + +local function runTestScript(scriptContent: string): (boolean, string) + local scriptName = createTempScript() + local projectRoot = process.cwd + + -- Write the test script to the project root (where lune is available) + fs.writeFile(`{projectRoot}/{scriptName}`, scriptContent) + + -- Run the test script with lune from the project root + local result = process.exec("lune", { "run", scriptName }, { + cwd = projectRoot, + }) + + local output = (result.stdout or "") .. (result.stderr or "") + + -- Clean up the temp script + fs.removeFile(`{projectRoot}/{scriptName}`) + + return result.ok, output +end + return function() describe("fail() function", function() it("should be available in test environment", function() expect(fail).to.be.a("function") end) - -- Note: We can't easily test that fail() actually fails a test from within - -- a test, because calling fail() would fail the current test. The following - -- tests verify fail() exists and is callable. + -- All subprocess tests are combined into a single test to avoid parallel execution issues. + -- Each subprocess creates its own isolated Testable instance. + it("should cause tests to fail correctly (subprocess tests)", function() + -- Test 1: fail without message + do + local scriptContent = [[ + local Testable = require("./Source/Testable") + + local tests = { + { + Name = "FailingTest", + Func = function() + describe("fail test", function() + it("should fail", function() + fail() + end) + end) + end, + }, + } + + local _, passed = Testable.run(tests) + local process = require("@lune/process") + process.exit(if passed then 0 else 1) + ]] + local success, output = runTestScript(scriptContent) + expect(success).to.equal(false) + expect(string.find(output, "fail() was called", 1, true)).to.be.ok() + end + + -- Test 2: fail with custom message + do + local scriptContent = [[ + local Testable = require("./Source/Testable") + + local tests = { + { + Name = "FailingTest", + Func = function() + describe("fail test", function() + it("should fail with message", function() + fail("custom failure message") + end) + end) + end, + }, + } + + local _, passed = Testable.run(tests) + local process = require("@lune/process") + process.exit(if passed then 0 else 1) + ]] + local success, output = runTestScript(scriptContent) + expect(success).to.equal(false) + expect(string.find(output, "custom failure message", 1, true)).to.be.ok() + end + + -- Test 3: fail should not affect other passing tests + do + local scriptContent = [[ + local Testable = require("./Source/Testable") + + local tests = { + { + Name = "MixedTest", + Func = function() + describe("mixed tests", function() + it("should pass first", function() + expect(true).to.equal(true) + end) + + it("should fail", function() + fail("intentional failure") + end) + + it("should pass after failure", function() + expect(true).to.equal(true) + end) + end) + end, + }, + } + + local results, passed = Testable.run(tests) + print("PASS_COUNT:" .. results.successCount) + print("FAIL_COUNT:" .. results.failureCount) + local process = require("@lune/process") + process.exit(if passed then 0 else 1) + ]] + local success, output = runTestScript(scriptContent) + expect(success).to.equal(false) + expect(string.find(output, "PASS_COUNT:2", 1, true)).to.be.ok() + expect(string.find(output, "FAIL_COUNT:1", 1, true)).to.be.ok() + end + + -- Test 4: fail should work when called conditionally + do + local scriptContent = [[ + local Testable = require("./Source/Testable") + + local tests = { + { + Name = "ConditionalFailTest", + Func = function() + describe("conditional fail", function() + it("should fail when condition is met", function() + local shouldFail = true + if shouldFail then + fail("condition was met") + end + end) + end) + end, + }, + } + + local _, passed = Testable.run(tests) + local process = require("@lune/process") + process.exit(if passed then 0 else 1) + ]] + local success, output = runTestScript(scriptContent) + expect(success).to.equal(false) + expect(string.find(output, "condition was met", 1, true)).to.be.ok() + end + end) end) end diff --git a/Tests/LifecycleTest.spec.luau b/Tests/LifecycleTest.spec.luau index 8fddc13..5c001c4 100644 --- a/Tests/LifecycleTest.spec.luau +++ b/Tests/LifecycleTest.spec.luau @@ -1,6 +1,6 @@ --[[ -LifecycleTest +LifecycleTest.spec Tests for lifecycle hooks in the Testable framework including beforeEach, afterEach, beforeAll, and afterAll hooks. diff --git a/Tests/VersionUpdateTest.spec.luau b/Tests/VersionUpdateTest.spec.luau index 20814b2..e071373 100644 --- a/Tests/VersionUpdateTest.spec.luau +++ b/Tests/VersionUpdateTest.spec.luau @@ -1,6 +1,6 @@ --[[ -VersionUpdateTest +VersionUpdateTest.spec Tests for the EnsureProperVersionUpdate script. Validates semantic versioning enforcement, version bump validation, and edge cases for version format handling. @@ -172,9 +172,9 @@ end local function copyScriptsToRepo(repoPath: string) fs.writeDir(`{repoPath}/Scripts`) fs.writeDir(`{repoPath}/Scripts/Helpers`) - local scriptContent = fs.readFile("Scripts/EnsureProperVersionUpdate.luau") + local scriptContent = fs.readFile("Submodules/luau-cicd/Scripts/EnsureProperVersionUpdate.luau") fs.writeFile(`{repoPath}/Scripts/EnsureProperVersionUpdate.luau`, scriptContent) - local helperContent = fs.readFile("Scripts/Helpers/parseVersion.luau") + local helperContent = fs.readFile("Submodules/luau-cicd/Scripts/Helpers/parseVersion.luau") fs.writeFile(`{repoPath}/Scripts/Helpers/parseVersion.luau`, helperContent) end diff --git a/VERSION b/VERSION index bbdeab6..6e8bf73 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.5 +0.1.0 diff --git a/init.luau b/init.luau index aaa8b22..0ccf7e4 100644 --- a/init.luau +++ b/init.luau @@ -1,9 +1 @@ ---[[ - -testable - -Re-exports the Testable module for package consumers. - ---]] - -return require("./testable/Source/Testable") +return require("@self/Source/Testable") diff --git a/stylua.toml b/stylua.toml index 84ef959..9084e15 100644 --- a/stylua.toml +++ b/stylua.toml @@ -7,7 +7,6 @@ quote_style = "AutoPreferDouble" call_parentheses = "Always" collapse_simple_statement = "Never" space_after_function_names = "Never" -block_newline_gaps = "Never" [sort_requires] enabled = true diff --git a/testable.code-workspace b/testable.code-workspace new file mode 100644 index 0000000..8265834 --- /dev/null +++ b/testable.code-workspace @@ -0,0 +1,45 @@ +{ + "extensions": { + "recommendations": ["JohnnyMorganz.luau-lsp", "JohnnyMorganz.stylua"] + }, + "folders": [ + { + "path": "." + } + ], + "settings": { + "[lua]": { + "editor.defaultFormatter": "JohnnyMorganz.stylua", + "editor.formatOnSave": true + }, + "[luau]": { + "editor.defaultFormatter": "JohnnyMorganz.stylua", + "editor.formatOnSave": true + }, + "editor.rulers": [ + { + "color": "#ffffff10", + "column": 90 // For comments + }, + { + "color": "#ffffff10", + "column": 120 // For code + } + ], + "luau-lsp.completion.autocompleteEnd": true, + "luau-lsp.completion.enableFragmentAutocomplete": true, + "luau-lsp.completion.fillCallArguments": false, + "luau-lsp.completion.imports.enabled": true, + "luau-lsp.completion.imports.separateGroupsWithLine": true, + "luau-lsp.completion.imports.stringRequires.enabled": true, + "luau-lsp.fflags.enableNewSolver": true, + "luau-lsp.hover.multilineFunctionDefinitions": true, + "luau-lsp.hover.showTableKinds": true, + "luau-lsp.inlayHints.hideHintsForErrorTypes": true, + "luau-lsp.sourcemap.autogenerate": true, + "luau-lsp.sourcemap.enabled": true, + "luau-lsp.sourcemap.rojoProjectFile": "default.project.json", + "luau-lsp.sourcemap.sourcemapFile": "sourcemap.json", + "search.useIgnoreFiles": false + } +} diff --git a/wally.toml b/wally.toml index 5f26f07..7662503 100644 --- a/wally.toml +++ b/wally.toml @@ -1,7 +1,7 @@ [package] name = "horsenuggets/testable" description = "A Luau testing framework based off of TestEZ." -version = "0.0.5" +version = "0.1.0" license = "MIT" realm = "shared" registry = "https://github.com/UpliftGames/wally-index"