diff --git a/.gdunit4_action/.runsettings b/.gdunit4_action/.runsettings new file mode 100644 index 0000000..f81d390 --- /dev/null +++ b/.gdunit4_action/.runsettings @@ -0,0 +1,39 @@ + + + + 1 + ./TestResults + net7.0;net8.0 + 180000 + true + + + + + + + detailed + + + + + test-result.html + + + + + test-result.trx + + + + + + + + + --audio-driver Dummy --display-driver x11 --rendering-driver opengl3 --screen 0 --verbose + + FullyQualifiedName + + diff --git a/.gdunit4_action/check-coredump/action.yml b/.gdunit4_action/check-coredump/action.yml new file mode 100644 index 0000000..057f4df --- /dev/null +++ b/.gdunit4_action/check-coredump/action.yml @@ -0,0 +1,54 @@ +name: check-coredump +description: "Checks for created coredumps" + +inputs: + artifact-name: + description: "Name of artifact to upload." + required: true + +runs: + using: composite + steps: + + - name: "Check for existing coredumps" + shell: bash + if: always() + id: collectlogs + # we have actual no debug infos and coredumpctl gdb will fail, we just ignore for this for the moment + continue-on-error: true + run: | + # wait for in progress coredumps + sleep 10 + if coredumpctl list; then + echo "coredumps=true" >>$GITHUB_OUTPUT + + sudo coredumpctl gdb <<<" + set verbose on + set trace-commands on + show debug-file-directory + printf "'"'"query = '%s'\n\n"'"'", debug_query_string + frame function ExceptionalCondition + printf "'"'"condition = '%s'\n"'"'", conditionName + up 1 + l + info args + info locals + bt full + " 2>&1 | tee stacktrace.log + fi + true + + - name: "Upload Coredumps" + if: always() && steps.collectlogs.outputs.coredumps == 'true' + uses: actions/upload-artifact@v7 + with: + name: Coredump_${{ inputs.artifact-name }} + path: /var/lib/systemd/coredump + + - name: "Cleanup Coredumps" + shell: bash + if: always() && steps.collectlogs.outputs.coredumps == 'true' + run: | + sudo journalctl --rotate + sudo journalctl --vacuum-time=5s + sudo rm -rf /var/lib/systemd/coredump/* diff --git a/.gdunit4_action/godot-install/action.yml b/.gdunit4_action/godot-install/action.yml new file mode 100644 index 0000000..b77748a --- /dev/null +++ b/.gdunit4_action/godot-install/action.yml @@ -0,0 +1,67 @@ +name: install-godot-binary +description: 'Installs the Godot Runtime' + +inputs: + godot-version: + description: 'The Godot engine version' + type: string + required: true + godot-status-version: + description: 'The Godot engine status version' + type: string + required: true + godot-net: + required: false + type: boolean + default: false + install-path: + type: string + required: true + +runs: + using: composite + steps: + - name: 'Download and Install Godot ${{ inputs.godot-version }}-${{ inputs.godot-status-version }}' + if: steps.godot-cache-binary.outputs.cache-hit != 'true' + continue-on-error: false + shell: bash + run: | + mkdir -p ${{ inputs.install-path }} + chmod 770 ${{ inputs.install-path }} + DIR="$HOME/.config/godot" + if [ ! -d "$DIR" ]; then + mkdir -p "$DIR" + chmod 770 "$DIR" + fi + + DOWNLOAD_URL=https://github.com/godotengine/godot-builds/releases/download/${{ inputs.godot-version }}-${{ inputs.godot-status-version }} + GODOT_BIN=Godot_v${{ inputs.godot-version }}-${{ inputs.godot-status-version }}_linux.x86_64 + if ${{inputs.godot-net == 'true'}}; then + GODOT_BIN=Godot_v${{ inputs.godot-version }}-${{ inputs.godot-status-version }}_mono_linux_x86_64 + fi + + GODOT_PACKAGE=$GODOT_BIN.zip + echo "Download URL: $DOWNLOAD_URL/$GODOT_PACKAGE" + wget --progress=bar:force:noscroll $DOWNLOAD_URL/$GODOT_PACKAGE -P ${{ inputs.install-path }} + unzip ${{ inputs.install-path }}/$GODOT_PACKAGE -d ${{ inputs.install-path }} + rm -rf ${{ inputs.install-path }}/$GODOT_PACKAGE + + if ${{runner.OS == 'Linux'}}; then + echo "Run linux part" + + if ${{inputs.godot-net == 'true'}}; then + mv ${{ inputs.install-path }}/$GODOT_BIN/* ${{ inputs.install-path }} + rmdir ${{ inputs.install-path }}/$GODOT_BIN/ + fi + + mv ${{ inputs.install-path }}/Godot_v* ${{ inputs.install-path }}/godot + chmod u+x ${{ inputs.install-path }}/godot + echo "${{ inputs.install-path }}/godot" + else + echo "Run windows part" + pwd + mv ${{ inputs.install-path }}/$GODOT_BIN ${{ inputs.install-path }}/godot.exe + chmod u+x ${{ inputs.install-path }}/godot.exe + ${{ inputs.install-path }}/godot.exe --version + echo "${{ inputs.install-path }}/godot.exe" + fi diff --git a/.gdunit4_action/publish-test-report/action.yml b/.gdunit4_action/publish-test-report/action.yml new file mode 100644 index 0000000..a8c4397 --- /dev/null +++ b/.gdunit4_action/publish-test-report/action.yml @@ -0,0 +1,29 @@ +name: publish-test-report +description: 'Publishes the GdUnit test results' + +inputs: + report-name: + description: 'Name of the check run which will be created.' + required: true + project_dir: + description: 'The working directory to collect the results.' + required: true + default: './' + reporter: + description: 'The report format to build the report' + required: false + default: 'java-junit' + + +runs: + using: composite + steps: + - name: 'Publish Test Results' + uses: dorny/test-reporter@v2 + with: + name: ${{ inputs.report-name }} + path: '${{ inputs.project_dir }}reports/**/*.xml' + reporter: ${{ inputs.reporter }} + fail-on-error: 'false' + fail-on-empty: 'false' + use-actions-summary: 'false' diff --git a/.gdunit4_action/unit-test/index.js b/.gdunit4_action/unit-test/index.js new file mode 100644 index 0000000..6524176 --- /dev/null +++ b/.gdunit4_action/unit-test/index.js @@ -0,0 +1,131 @@ +const pathLib = require("path"); +const {spawnSync } = require("node:child_process"); + +const RETURN_SUCCESS = 0; +const RETURN_ERROR = 100; +const RETURN_WARNING = 101; +const RETURN_ERROR_HEADLESS_NOT_SUPPORTED = 103; +const RETURN_ERROR_GODOT_VERSION_NOT_SUPPORTED = 104; +const RETURN_ERROR_ABNORMAL_TERMINATED = 444; + +function getProjectPath(project_dir) { + if (!process.env.GITHUB_WORKSPACE) { + throw new Error("GITHUB_WORKSPACE environment variable not set"); + } + return pathLib.join(process.env.GITHUB_WORKSPACE, project_dir); +} + + +function console_info(message) { + console.log('\x1b[94m', message, '\x1b[0m'); +} + + +async function runTests(exeArgs, core) { + try { + const { + project_dir = '', + paths = "", + arguments = '', + timeout = 10, + retries = 0, + warningsAsErrors = false + } = exeArgs; + // Split by newline or comma, map, trim, and filter empty strings + const pathsArray = paths.split(/[\r\n,]+/).map((entry) => entry.trim()).filter(Boolean); + + // verify support of multi paths/fixed since v4.2.1 + if (pathsArray.length > 1 && process.env.GDUNIT_VERSION === "v4.2.0") { + core.warning(`Multiple path arguments not supported by GdUnit ${process.env.GDUNIT_VERSION}, please upgrade to GdUnit4 v4.2.1`); + pathsArray.length = 1; + core.warning(`Use only the first entry of paths, ${pathsArray}!`); + } + + const args = [ + "--auto-servernum", + "./addons/gdUnit4/runtest.sh", + "--audio-driver Dummy", + "--display-driver x11", + "--rendering-driver opengl3", + "--single-window", + "--continue", + ...pathsArray.map((path) => `--add ${path}`), + `${arguments}` + ]; + + const working_dir = getProjectPath(project_dir); + console_info(`Running GdUnit4 ${process.env.GDUNIT_VERSION} tests at ${working_dir} with arguments: ${args}`); + console_info(`project_dir: ${project_dir}`); + console_info(`arguments: ${arguments}`); + console_info(`timeout: ${timeout}m`); + console_info(`retries: ${retries}`); + console_info(`warningsAsErrors: ${warningsAsErrors}`); + + let retriesCount = 0; + let exitCode = 0; + + while (retriesCount <= retries) { + const child = spawnSync("xvfb-run", args, { + cwd: working_dir, + timeout: timeout * 1000 * 60, + encoding: "utf-8", + shell: true, + stdio: ["inherit", "inherit", "inherit"], + env: process.env, + }); + + // Handle spawn errors + if (child.error) { + core.setFailed(`Run Godot process ends with error: ${child.error}`); + return RETURN_ERROR_ABNORMAL_TERMINATED; + } + + exitCode = child.status; + + if (exitCode === RETURN_SUCCESS) { + break; // Exit loop if successful + } + retriesCount++; + if (retriesCount <= retries) { + core.warning(`Test run failed, retrying... ${retriesCount} of ${retries}`); + } + } + + switch (exitCode) { + case RETURN_SUCCESS: + if (retriesCount > 0 && retries > 0) { + core.warning(`The tests was successfully after ${retriesCount} retries with exit code: ${exitCode}`); + } else { + console_info(`The tests was successfully with exit code: ${exitCode}`); + } + break; + case RETURN_ERROR: + core.setFailed(`The tests was failed after ${retries} retries with exit code: ${exitCode}`); + break; + case RETURN_WARNING: + if (warningsAsErrors === true) { + core.setFailed(`Tests completed with warnings (treated as errors)`); + } else { + core.warning('Tests completed successfully with warnings'); + } + break; + case RETURN_ERROR_HEADLESS_NOT_SUPPORTED: + core.setFailed('Headless mode not supported'); + break; + case RETURN_ERROR_GODOT_VERSION_NOT_SUPPORTED: + core.setFailed('Godot version not supported'); + break; + default: + core.setFailed(`Tests failed with unknown error code: ${exitCode}`); + } + + return exitCode; + } catch (error) { + core.setFailed(`Tests failed: ${error.message}`); + return RETURN_ERROR; + } +} + +module.exports = { + runTests +}; diff --git a/.gdunit4_action/upload-test-report/action.yml b/.gdunit4_action/upload-test-report/action.yml new file mode 100644 index 0000000..32bfcda --- /dev/null +++ b/.gdunit4_action/upload-test-report/action.yml @@ -0,0 +1,24 @@ +name: 'upload test report' +description: 'Uploads the GdUnit test reports' + +inputs: + report-name: + description: 'Name of the report to be upload.' + required: true + project_dir: + description: 'The working directory to collect the reports.' + required: true + default: './' + +runs: + using: composite + steps: + - name: 'Upload Test Artifacts' + uses: actions/upload-artifact@v7 + with: + retention-days: 10 + name: artifact_${{ inputs.report-name }} + path: | + ${{ inputs.project_dir }}reports/** + /var/lib/systemd/coredump/** + /var/log/syslog diff --git a/.gdunit4_action/versioning/action.yml b/.gdunit4_action/versioning/action.yml new file mode 100644 index 0000000..d9c7c87 --- /dev/null +++ b/.gdunit4_action/versioning/action.yml @@ -0,0 +1,130 @@ +name: 'Determine GdUnit4 Version' +description: 'Determines the actual GdUnit4 version that will be used' + +inputs: + version: + description: 'The version of GdUnit4 to use (e.g., "v4.2.0", "latest", "master", "installed")' + required: true + project_dir: + description: 'The project directory' + required: true + default: './' + +outputs: + gdunit-version: + description: 'The determined GdUnit4 version' + value: ${{ steps.determine.outputs.version }} + +runs: + using: 'composite' + steps: + - name: 'Determine Version' + id: determine + shell: bash + run: | + echo "" + echo -e "\e[34mRun Determine GdUnit4 Version\e[0m" + + echo -e "\e[33mVersion Input: \e[0m ${{ inputs.version }}" + echo -e "\e[33mProject Directory: \e[0m ${{ inputs.project_dir }}" + echo "" + + cd "${{ inputs.project_dir }}" + + # Function to extract version from plugin.cfg + extract_version_from_plugin() { + local plugin_file=$1 + if [[ -f "${plugin_file}" ]]; then + version=$(grep -oP '^version="\K[^"]+' "${plugin_file}") + if [[ -n "${version}" ]]; then + # Add 'v' prefix if not present + if [[ ! "${version}" =~ ^v ]]; then + version="v${version}" + fi + echo "${version}" + return 0 + fi + fi + return 1 + } + + # Determine GdUnit4 version based on input + if [[ "${{ inputs.version }}" == "installed" ]]; then + echo -e "\e[33mChecking installed GdUnit4 plugin version... \e[0m" + + if [[ -f ./addons/gdUnit4/plugin.cfg ]]; then + gdunit_version=$(extract_version_from_plugin "./addons/gdUnit4/plugin.cfg") + + if [[ -n "${gdunit_version}" ]]; then + echo -e "\e[92m✓ Found installed GdUnit4 version: ${gdunit_version} \e[0m" + echo "version=${gdunit_version}" >> $GITHUB_OUTPUT + echo "GDUNIT_VERSION=${gdunit_version}" >> $GITHUB_ENV + exit 0 + else + echo -e "\e[31m✗ Error: Could not extract version from ./addons/gdUnit4/plugin.cfg \e[0m" + exit 1 + fi + else + echo -e "\e[31m✗ Error: GdUnit4 plugin not found at ./addons/gdUnit4/plugin.cfg \e[0m" + echo -e "\e[31m Make sure the plugin is installed before using version='installed' \e[0m" + exit 1 + fi + + elif [[ "${{ inputs.version }}" == "latest" ]]; then + echo -e "\e[33mResolving 'latest' GdUnit4 version from GitHub... \e[0m" + + gdunit_version=$(git ls-remote --refs --tags https://github.com/godot-gdunit-labs/gdUnit4 v* | sort -t '/' -k 3 -V | tail -n 1 | cut -d '/' -f 3) + + if [[ -n "${gdunit_version}" ]]; then + echo -e "\e[92m✓ Resolved latest version: ${gdunit_version} \e[0m" + echo "version=${gdunit_version}" >> $GITHUB_OUTPUT + echo "GDUNIT_VERSION=${gdunit_version}" >> $GITHUB_ENV + exit 0 + else + echo -e "\e[31m✗ Error: Could not resolve latest version from GitHub \e[0m" + exit 1 + fi + + else + # Custom branch/tag specified + echo -e "\e[33mFetching GdUnit4 version from branch/tag '${{ inputs.version }}'... \e[0m" + + # Check if it's a tag + if git ls-remote --tags https://github.com/godot-gdunit-labs/gdUnit4 | grep -q "refs/tags/${{ inputs.version }}$"; then + echo -e "\e[92m✓ Tag '${{ inputs.version }}' found \e[0m" + # Check if it's a branch + elif git ls-remote --heads https://github.com/godot-gdunit-labs/gdUnit4 | grep -q "refs/heads/${{ inputs.version }}$"; then + echo -e "\e[92m✓ Branch '${{ inputs.version }}' found \e[0m" + else + echo -e "\e[31m✗ Error: Branch/tag '${{ inputs.version }}' does not exist on GitHub \e[0m" + echo -e "\e[31m Please verify that the branch/tag exists at: https://github.com/godot-gdunit-labs/gdUnit4 \e[0m" + echo "" + echo "Available recent tags:" >&2 + git ls-remote --tags https://github.com/godot-gdunit-labs/gdUnit4 | grep -oP 'refs/tags/\K.*' | grep -v '\^{}' | sort -V | tail -n 10 >&2 + exit 1 + fi + + # Clone the specific branch to get the plugin.cfg + CLONE_DIR="./.gdunit4-version-${{ inputs.version }}" + + if git clone --quiet --depth 1 --branch "${{ inputs.version }}" --single-branch https://github.com/godot-gdunit-labs/gdUnit4 "${CLONE_DIR}" 2>/dev/null; then + + gdunit_version=$(extract_version_from_plugin "${CLONE_DIR}/addons/gdUnit4/plugin.cfg") + + if [[ -n "${gdunit_version}" ]]; then + echo -e "\e[92m✓ Found GdUnit4 version from branch '${{ inputs.version }}': ${gdunit_version} \e[0m" + echo "version=${gdunit_version}" >> $GITHUB_OUTPUT + echo "GDUNIT_VERSION=${gdunit_version}" >> $GITHUB_ENV + exit 0 + else + echo -e "\e[31m✗ Error: Could not extract version from plugin.cfg in branch '${{ inputs.version }}' \e[0m" + rm -rf "${CLONE_DIR}" + exit 1 + fi + else + echo -e "\e[31m✗ Error: Could not clone branch/tag '${{ inputs.version }}' from GitHub \e[0m" + echo -e "\e[31m Please verify that the branch/tag exists at: https://github.com/godot-gdunit-labs/gdUnit4 \e[0m" + rm -rf "${CLONE_DIR}" 2>/dev/null + exit 1 + fi + fi diff --git a/.gdunit4_action/versioning/check/action.yml b/.gdunit4_action/versioning/check/action.yml new file mode 100644 index 0000000..90d7a93 --- /dev/null +++ b/.gdunit4_action/versioning/check/action.yml @@ -0,0 +1,114 @@ +name: 'Verify GdUnit4 Version Compatibility' +description: 'Verifies that the Godot and GdUnit4 version combination is compatible' + +inputs: + godot-version: + description: 'The Godot version' + required: true + gdunit-version: + description: 'The GdUnit4 version' + required: true + +outputs: + exit_code: + description: 'Exit code from compatibility check' + value: ${{ steps.check-compatibility.outputs.exit_code }} + +runs: + using: 'composite' + steps: + - name: 'Check Compatibility' + id: check-compatibility + shell: bash + run: | + echo "" + echo -e "\e[34mRun Version Compatibility Check\e[0m" + + GODOT_VERSION="${{ inputs.godot-version }}" + GDUNIT_VERSION="${{ inputs.gdunit-version }}" + + echo -e "\e[33mConfigured Godot Version:\e[0m ${GODOT_VERSION}" + echo -e "\e[33mConfigured GdUnit4 Version:\e[0m ${GDUNIT_VERSION}" + echo "" + + # Load compatibility matrix + COMPATIBILITY_FILE="./.gdunit4_action/versioning/check/compatibility_matrix.json" + + if [[ ! -f "${COMPATIBILITY_FILE}" ]]; then + echo -e "\e[31m✗ Error: Compatibility matrix file not found\e[0m" + echo "exit_code=1" >> "$GITHUB_OUTPUT" + exit 1 + fi + + # Normalize versions - remove 'v' prefix + GODOT_VER=${GODOT_VERSION#v} + GDUNIT_VER=${GDUNIT_VERSION#v} + + # Function to compare version numbers (returns 0 if $1 >= $2) + version_ge() { + printf '%s\n%s\n' "$2" "$1" | sort -V -C + return $? + } + + # Function to compare version numbers (returns 0 if $1 <= $2) + version_le() { + printf '%s\n%s\n' "$1" "$2" | sort -V -C + return $? + } + + # Find matching GdUnit4 version in matrix + ALL_VERSIONS=$(grep -oP '^\s*"\K[0-9]+\.[0-9]+\.[0-9]+(?=":)' "${COMPATIBILITY_FILE}" | sort -V -r) + + # Find the closest version that is <= GDUNIT_VER + for ver in $ALL_VERSIONS; do + if version_le "$ver" "$GDUNIT_VER"; then + echo -e "\e[92m✓ Using compatibility rules for GdUnit4 v${ver}\e[0m" + GODOT_MIN=$(grep -A 1 "\"${ver}\":" "${COMPATIBILITY_FILE}" | grep "godot_min" | cut -d'"' -f4) + GODOT_MAX=$(grep -A 2 "\"${ver}\":" "${COMPATIBILITY_FILE}" | grep "godot_max" | cut -d'"' -f4) + break + fi + done + + # If still no match found, use default + if [[ -z "$GODOT_MIN" ]]; then + echo -e "\e[33m⚠️ No specific version found, using default compatibility rules\e[0m" + GODOT_MIN=$(grep -A 1 "\"default\":" "${COMPATIBILITY_FILE}" | grep "godot_min" | cut -d'"' -f4) + fi + + # Check if Godot version meets the minimum requirement + if ! version_ge "$GODOT_VER" "$GODOT_MIN"; then + echo -e "\e[31m✗ Incompatible version combination detected!\e[0m" + echo -e "\e[31m GdUnit4 v${GDUNIT_VER} requires at least Godot v${GODOT_MIN}, but you are using Godot v${GODOT_VER}\e[0m" + echo "" + REFERENCE=$(grep -oP '"reference":\s*"\K[^"]+' "${COMPATIBILITY_FILE}") + echo -e "\e[33mPlease check the compatibility matrix at:\e[0m" + echo -e "\e[34m${REFERENCE}\e[0m" + echo -e "\e[34m========================================\e[0m" + echo "exit_code=78" >> "$GITHUB_OUTPUT" + exit 78 + fi + + # Check if Godot version exceeds maximum (if max is defined) + if [[ -n "$GODOT_MAX" ]] && ! version_le "$GODOT_VER" "$GODOT_MAX"; then + echo -e "\e[31m✗ Incompatible version combination detected!\e[0m" + echo -e "\e[31m GdUnit4 v${GDUNIT_VER} supports Godot v${GODOT_MIN} to v${GODOT_MAX}, but you are using Godot v${GODOT_VER}\e[0m" + echo "" + REFERENCE=$(grep -oP '"reference":\s*"\K[^"]+' "${COMPATIBILITY_FILE}") + echo -e "\e[33mPlease check the compatibility matrix at:\e[0m" + echo -e "\e[34m${REFERENCE}\e[0m" + echo -e "\e[34m========================================\e[0m" + echo "exit_code=78" >> "$GITHUB_OUTPUT" + exit 78 + fi + + # All checks passed + echo -e "\e[92m✓ Configured GdUnit4 version is compatible!\e[0m" + if [[ -n "$GODOT_MAX" ]]; then + echo -e "\e[92m Godot v${GODOT_MIN} <= v${GODOT_VER} <= v${GODOT_MAX} ✓\e[0m" + else + echo -e "\e[92m Godot v${GODOT_VER} >= v${GODOT_MIN} ✓\e[0m" + fi + + echo -e "\e[34m========================================\e[0m" + echo "exit_code=0" >> "$GITHUB_OUTPUT" + exit 0 diff --git a/.gdunit4_action/versioning/check/compatibility_matrix.json b/.gdunit4_action/versioning/check/compatibility_matrix.json new file mode 100644 index 0000000..fdfa642 --- /dev/null +++ b/.gdunit4_action/versioning/check/compatibility_matrix.json @@ -0,0 +1,33 @@ +{ + "reference": "https://github.com/godot-gdunit-labs/gdUnit4#compatibility-overview", + "default": { + "godot_min": "4.5", + "godot_max": "5.0" + }, + "versions": { + "6.0.0": { + "godot_min": "4.5", + "godot_max": "5.0" + }, + "5.0.0": { + "godot_min": "4.3", + "godot_max": "4.4.999" + }, + "4.4.0": { + "godot_min": "4.2", + "godot_max": "4.4.999" + }, + "4.3.0": { + "godot_min": "4.2", + "godot_max": "4.3.999" + }, + "4.2.1": { + "godot_min": "4.1", + "godot_max": "4.2.999" + }, + "4.2.0": { + "godot_min": "4.0", + "godot_max": "4.1.999" + } + } +}