diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..e72a1e2 --- /dev/null +++ b/.clang-format @@ -0,0 +1,74 @@ +--- +# This file is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# Generated from CLion C/C++ Code Style settings +BasedOnStyle: LLVM +AccessModifierOffset: -2 +AlignAfterOpenBracket: DontAlign +AlignConsecutiveAssignments: false +AlignOperands: Align +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Always +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: true +AlignTrailingComments: false +AlwaysBreakAfterReturnType: All +AlwaysBreakTemplateDeclarations: MultiLine +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterUnion: false + BeforeCatch: true + BeforeElse: true + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: true +BreakBeforeBinaryOperators: None +BreakBeforeTernaryOperators: false +BreakConstructorInitializers: AfterColon +BreakInheritanceList: AfterColon +ColumnLimit: 0 +CompactNamespaces: false +ContinuationIndentWidth: 2 +IndentCaseLabels: true +IndentPPDirectives: BeforeHash +IndentWidth: 2 +KeepEmptyLinesAtTheStartOfBlocks: false +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: All +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: true +PointerAlignment: Right +ReflowComments: true +SpaceAfterCStyleCast: true +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: true +SpaceBeforeCtorInitializerColon: false +SpaceBeforeInheritanceColon: false +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: Never +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 2 +Cpp11BracedListStyle: false +UseTab: Never diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..2d028b2 --- /dev/null +++ b/.flake8 @@ -0,0 +1,7 @@ +[flake8] +filename = + *.py, + *.pys +max-line-length = 120 +extend-exclude = + venv/ diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6eb0cda --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,48 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +version: 2 +updates: + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "daily" + time: "08:00" + open-pull-requests-limit: 10 + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + time: "08:30" + open-pull-requests-limit: 10 + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + time: "09:00" + open-pull-requests-limit: 10 + + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "daily" + time: "09:30" + open-pull-requests-limit: 10 + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + time: "10:00" + open-pull-requests-limit: 10 + + - package-ecosystem: "gitsubmodule" + directory: "/" + schedule: + interval: "daily" + time: "10:30" + open-pull-requests-limit: 10 diff --git a/.github/label-actions.yml b/.github/label-actions.yml new file mode 100644 index 0000000..2949601 --- /dev/null +++ b/.github/label-actions.yml @@ -0,0 +1,49 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# Configuration for Label Actions - https://github.com/dessant/label-actions + +added: + comment: > + This feature has been added and will be available in the next release. +fixed: + comment: > + This issue has been fixed and will be available in the next release. +invalid:duplicate: + comment: > + :wave: @{issue-author}, this appears to be a duplicate of a pre-existing issue. + close: true + lock: true + unlabel: 'status:awaiting-triage' + +-invalid:duplicate: + reopen: true + unlock: true + +invalid:support: + comment: > + :wave: @{issue-author}, we use the issue tracker exclusively for bug reports. + However, this issue appears to be a support request. Please use our + [Support Center](https://app.lizardbyte.dev/support) for support issues. Thanks. + close: true + lock: true + lock-reason: 'off-topic' + unlabel: 'status:awaiting-triage' + +-invalid:support: + reopen: true + unlock: true + +invalid:template-incomplete: + issues: + comment: > + :wave: @{issue-author}, please edit your issue to complete the template with + all the required info. Your issue will be automatically closed in 5 days if + the template is not completed. Thanks. + prs: + comment: > + :wave: @{issue-author}, please edit your PR to complete the template with + all the required info. Your PR will be automatically closed in 5 days if + the template is not completed. Thanks. diff --git a/.github/workflows/auto-create-pr.yml b/.github/workflows/auto-create-pr.yml new file mode 100644 index 0000000..13705dd --- /dev/null +++ b/.github/workflows/auto-create-pr.yml @@ -0,0 +1,35 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# This workflow creates a PR automatically when anything is merged/pushed into the `nightly` branch. The PR is created +# against the `master` (default) branch. + +name: Auto create PR + +on: + push: + branches: + - 'nightly' + +jobs: + create_pr: + if: startsWith(github.repository, 'LizardByte/') + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Create Pull Request + uses: repo-sync/pull-request@v2 + with: + source_branch: "" # should be "nightly" as it's the triggering branch + destination_branch: "master" + pr_title: "Pulling ${{ github.ref_name }} into master" + pr_template: ".github/pr_release_template.md" + pr_assignee: "${{ secrets.GH_BOT_NAME }}" + pr_draft: true + pr_allow_empty: false + github_token: ${{ secrets.GH_BOT_TOKEN }} diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml new file mode 100644 index 0000000..733b4de --- /dev/null +++ b/.github/workflows/automerge.yml @@ -0,0 +1,64 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# This workflow will, first, automatically approve PRs created by @LizardByte-bot. Then it will automerge relevant PRs. + +name: Automerge PR + +on: + pull_request: + types: + - opened + - synchronize + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + autoapprove: + if: >- + contains(fromJson('["LizardByte-bot"]'), github.event.pull_request.user.login) && + contains(fromJson('["LizardByte-bot"]'), github.actor) && + startsWith(github.repository, 'LizardByte/') + runs-on: ubuntu-latest + steps: + - name: Autoapproving + uses: hmarr/auto-approve-action@v3 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Label autoapproved + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GH_BOT_TOKEN }} + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['autoapproved', 'autoupdate'] + }) + + automerge: + if: startsWith(github.repository, 'LizardByte/') + needs: [autoapprove] + runs-on: ubuntu-latest + + steps: + - name: Automerging + uses: pascalgn/automerge-action@v0.15.6 + env: + BASE_BRANCHES: nightly + GITHUB_TOKEN: ${{ secrets.GH_BOT_TOKEN }} + GITHUB_LOGIN: ${{ secrets.GH_BOT_NAME }} + MERGE_LABELS: "!dependencies" + MERGE_METHOD: "squash" + MERGE_COMMIT_MESSAGE: "{pullRequest.title} (#{pullRequest.number})" + MERGE_DELETE_BRANCH: true + MERGE_ERROR_FAIL: true + MERGE_FILTER_AUTHOR: ${{ secrets.GH_BOT_NAME }} + MERGE_RETRIES: "240" # 1 hour + MERGE_RETRY_SLEEP: "15000" # 15 seconds diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..344789d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,133 @@ +--- +name: CI + +on: + pull_request: + branches: + - master + types: + - opened + - synchronize + - reopened + push: + branches: + - master + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + cmake_generator: "Unix Makefiles" + make_command: "make" + shell: "bash" + - os: ubuntu-latest + appindicator: "libayatana-appindicator3-dev" + cmake_generator: "Unix Makefiles" + make_command: "make" + shell: "bash" + - os: ubuntu-latest + appindicator: "libappindicator3-dev" + cmake_generator: "Unix Makefiles" + make_command: "make" + shell: "bash" + - os: windows-latest + cmake_generator: "MinGW Makefiles" + make_command: "mingw32-make" + shell: "msys2 {0}" + + name: Build (${{ matrix.os }} - ${{ matrix.appindicator || 'default' }}) + runs-on: ${{ matrix.os }} + defaults: + run: + shell: ${{ matrix.shell }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Dependencies Linux + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + ${{ matrix.appindicator }} \ + libnotify-dev + + - name: Setup Dependencies Windows + if: runner.os == 'Windows' + uses: msys2/setup-msys2@v2 + with: + update: true + install: >- + base-devel + make + mingw-w64-x86_64-binutils + mingw-w64-x86_64-cmake + mingw-w64-x86_64-toolchain + + - name: Setup python + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Python Path + id: python-path + run: | + if [ "${{ runner.os }}" = "Windows" ]; then + # replace backslashes with double backslashes + python_path=$(echo "${{ steps.setup-python.outputs.python-path }}" | sed 's/\\/\\\\/g') + else + python_path=${{ steps.setup-python.outputs.python-path }} + fi + + # step output + echo "python-path=${python_path}" + echo "python-path=${python_path}" >> $GITHUB_OUTPUT + + - name: Build + run: | + mkdir build + cd build + cmake \ + -G "${{ matrix.cmake_generator }}" \ + .. + ${{ matrix.make_command }} -j + + - name: Run tests + id: test + working-directory: build/tests + run: | + ./test_tray --gtest_color=yes + + - name: Generate gcov report + # any except canceled or skipped + if: always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure') + id: test_report + working-directory: build + run: | + ${{ steps.python-path.outputs.python-path }} -m pip install gcovr + ${{ steps.python-path.outputs.python-path }} -m gcovr -r .. \ + --exclude ../tests/ \ + --exclude ../third-party/ \ + --xml-pretty \ + -o coverage.xml + + - name: Upload coverage + # any except canceled or skipped + if: always() && (steps.test_report.outcome == 'success') + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + files: ./build/coverage.xml + flags: ${{ runner.os }} + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..ae52487 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,147 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# This workflow will analyze all supported languages in the repository using CodeQL Analysis. + +name: "CodeQL" + +on: + push: + branches: ["master", "nightly"] + pull_request: + branches: ["master", "nightly"] + schedule: + - cron: '00 12 * * 0' # every Sunday at 12:00 UTC + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + languages: + name: Get language matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.lang.outputs.result }} + continue: ${{ steps.continue.outputs.result }} + steps: + - name: Get repo languages + uses: actions/github-script@v7 + id: lang + with: + script: | + // CodeQL supports ['cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift'] + // Use only 'java' to analyze code written in Java, Kotlin or both + // Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + // Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + const supported_languages = ['cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift'] + + const remap_languages = { + 'c++': 'cpp', + 'c#': 'csharp', + 'kotlin': 'java', + 'typescript': 'javascript', + } + + const repo = context.repo + const response = await github.rest.repos.listLanguages(repo) + let matrix = { + "include": [] + } + + for (let [key, value] of Object.entries(response.data)) { + // remap language + if (remap_languages[key.toLowerCase()]) { + console.log(`Remapping language: ${key} to ${remap_languages[key.toLowerCase()]}`) + key = remap_languages[key.toLowerCase()] + } + if (supported_languages.includes(key.toLowerCase()) && + !matrix['include'].includes({"language": key.toLowerCase()})) { + console.log(`Found supported language: ${key}`) + matrix['include'].push({"language": key.toLowerCase()}) + } + } + + // print languages + console.log(`matrix: ${JSON.stringify(matrix)}`) + + return matrix + + - name: Continue + uses: actions/github-script@v7 + id: continue + with: + script: | + // if matrix['include'] is an empty list return false, otherwise true + const matrix = ${{ steps.lang.outputs.result }} // this is already json encoded + + if (matrix['include'].length == 0) { + return false + } else { + return true + } + + analyze: + name: Analyze + if: ${{ needs.languages.outputs.continue == 'true' }} + needs: [languages] + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.languages.outputs.matrix) }} + + steps: + - name: Maximize build space + uses: easimon/maximize-build-space@v8 + with: + root-reserve-mb: 20480 + remove-dotnet: ${{ (matrix.language == 'csharp' && 'false') || 'true' }} + remove-android: 'true' + remove-haskell: 'true' + remove-codeql: 'false' + remove-docker-images: 'true' + + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # yamllint disable-line rule:line-length + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # Pre autobuild + # create a file named .codeql-prebuild-${{ matrix.language }}.sh in the root of your repository + - name: Prebuild + run: | + # check if .qodeql-prebuild-${{ matrix.language }}.sh exists + if [ -f "./.codeql-prebuild-${{ matrix.language }}.sh" ]; then + echo "Running .codeql-prebuild-${{ matrix.language }}.sh" + ./.codeql-prebuild-${{ matrix.language }}.sh + fi + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/cpp-lint.yml b/.github/workflows/cpp-lint.yml new file mode 100644 index 0000000..921641c --- /dev/null +++ b/.github/workflows/cpp-lint.yml @@ -0,0 +1,120 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# Lint c++ source files and cmake files. + +name: C++ Lint + +on: + pull_request: + branches: [master, nightly] + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + clang-format: + name: Clang Format Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Find cpp files + id: find_files + run: | + # find files + found_files=$(find . -type f -iname "*.cpp" -o -iname "*.h" -o -iname "*.m" -o -iname "*.mm") + ignore_files=$(find . -type f -iname ".clang-format-ignore") + + # Loop through each C++ file + for file in $found_files; do + for ignore_file in $ignore_files; do + ignore_directory=$(dirname "$ignore_file") + # if directory of ignore_file is beginning of file + if [[ "$file" == "$ignore_directory"* ]]; then + echo "ignoring file: ${file}" + found_files="${found_files//${file}/}" + break 1 + fi + done + done + + # remove empty lines + found_files=$(echo "$found_files" | sed '/^\s*$/d') + + echo "found cpp files: ${found_files}" + + # do not quote to keep this as a single line + echo found_files=${found_files} >> $GITHUB_OUTPUT + + - name: Clang format lint + if: ${{ steps.find_files.outputs.found_files }} + uses: DoozyX/clang-format-lint-action@v0.16.2 + with: + source: ${{ steps.find_files.outputs.found_files }} + extensions: 'cpp,h,m,mm' + clangFormatVersion: 16 + style: file + inplace: false + + - name: Upload Artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: clang-format-fixes + path: ${{ steps.find_files.outputs.found_files }} + + cmake-lint: + name: CMake Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools cmakelang + + - name: Find cmake files + id: find_files + run: | + # find files + found_files=$(find . -type f -iname "CMakeLists.txt" -o -iname "*.cmake") + ignore_files=$(find . -type f -iname ".cmake-lint-ignore") + + # Loop through each C++ file + for file in $found_files; do + for ignore_file in $ignore_files; do + ignore_directory=$(dirname "$ignore_file") + # if directory of ignore_file is beginning of file + if [[ "$file" == "$ignore_directory"* ]]; then + echo "ignoring file: ${file}" + found_files="${found_files//${file}/}" + break 1 + fi + done + done + + # remove empty lines + found_files=$(echo "$found_files" | sed '/^\s*$/d') + + echo "found cmake files: ${found_files}" + + # do not quote to keep this as a single line + echo found_files=${found_files} >> $GITHUB_OUTPUT + + - name: Test with cmake-lint + run: | + cmake-lint --line-width 120 --tab-size 4 ${{ steps.find_files.outputs.found_files }} diff --git a/.github/workflows/issues-stale.yml b/.github/workflows/issues-stale.yml new file mode 100644 index 0000000..deb3d74 --- /dev/null +++ b/.github/workflows/issues-stale.yml @@ -0,0 +1,61 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# Manage stale issues and PRs. + +name: Stale Issues / PRs + +on: + schedule: + - cron: '00 10 * * *' + +jobs: + stale: + name: Check Stale Issues / PRs + if: startsWith(github.repository, 'LizardByte/') + runs-on: ubuntu-latest + steps: + - name: Stale + uses: actions/stale@v9 + with: + close-issue-message: > + This issue was closed because it has been stalled for 10 days with no activity. + close-pr-message: > + This PR was closed because it has been stalled for 10 days with no activity. + days-before-stale: 90 + days-before-close: 10 + exempt-all-assignees: true + exempt-issue-labels: 'added,fixed' + exempt-pr-labels: 'dependencies,l10n' + stale-issue-label: 'stale' + stale-issue-message: > + It seems this issue hasn't had any activity in the past 90 days. + If it's still something you'd like addressed, please let us know by leaving a comment. + Otherwise, to help keep our backlog tidy, we'll be closing this issue in 10 days. Thanks! + stale-pr-label: 'stale' + stale-pr-message: > + It looks like this PR has been idle for 90 days. + If it's still something you're working on or would like to pursue, + please leave a comment or update your branch. + Otherwise, we'll be closing this PR in 10 days to reduce our backlog. Thanks! + repo-token: ${{ secrets.GH_BOT_TOKEN }} + + - name: Invalid Template + uses: actions/stale@v9 + with: + close-issue-message: > + This issue was closed because the the template was not completed after 5 days. + close-pr-message: > + This PR was closed because the the template was not completed after 5 days. + days-before-stale: 0 + days-before-close: 5 + only-labels: 'invalid:template-incomplete' + stale-issue-label: 'invalid:template-incomplete' + stale-issue-message: > + Invalid issues template. + stale-pr-label: 'invalid:template-incomplete' + stale-pr-message: > + Invalid PR template. + repo-token: ${{ secrets.GH_BOT_TOKEN }} diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml new file mode 100644 index 0000000..aec6006 --- /dev/null +++ b/.github/workflows/issues.yml @@ -0,0 +1,25 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# Label and un-label actions using `../label-actions.yml`. + +name: Issues + +on: + issues: + types: [labeled, unlabeled] + discussion: + types: [labeled, unlabeled] + +jobs: + label: + name: Label Actions + if: startsWith(github.repository, 'LizardByte/') + runs-on: ubuntu-latest + steps: + - name: Label Actions + uses: dessant/label-actions@v4 + with: + github-token: ${{ secrets.GH_BOT_TOKEN }} diff --git a/.github/workflows/python-flake8.yml b/.github/workflows/python-flake8.yml new file mode 100644 index 0000000..e08ab10 --- /dev/null +++ b/.github/workflows/python-flake8.yml @@ -0,0 +1,38 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# Lint python files with flake8. + +name: flake8 + +on: + pull_request: + branches: [master, nightly] + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + flake8: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 # https://github.com/actions/setup-python + with: + python-version: '3.10' + + - name: Install dependencies + run: | + # pin flake8 before v6.0.0 due to removal of support for type comments (required for Python 2.7 type hints) + python -m pip install --upgrade pip setuptools "flake8<6" + + - name: Test with flake8 + run: | + python -m flake8 --verbose diff --git a/.github/workflows/yaml-lint.yml b/.github/workflows/yaml-lint.yml new file mode 100644 index 0000000..7e1fd46 --- /dev/null +++ b/.github/workflows/yaml-lint.yml @@ -0,0 +1,66 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# Lint yaml files. + +name: yaml lint + +on: + pull_request: + branches: [master, nightly] + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + yaml-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Find additional files + id: find-files + run: | + # space separated list of files + FILES=.clang-format + + # empty placeholder + FOUND="" + + for FILE in ${FILES}; do + if [ -f "$FILE" ] + then + FOUND="$FOUND $FILE" + fi + done + + echo "found=${FOUND}" >> $GITHUB_OUTPUT + + - name: yaml lint + id: yaml-lint + uses: ibiqlik/action-yamllint@v3 + with: + # https://yamllint.readthedocs.io/en/stable/configuration.html#default-configuration + config_data: | + extends: default + rules: + comments: + level: error + line-length: + max: 120 + truthy: + # GitHub uses "on" for workflow event triggers + # .clang-format file has options of "Yes" "No" that will be caught by this, so changed to "warning" + allowed-values: ['true', 'false', 'on'] + check-keys: true + level: warning + file_or_dir: . ${{ steps.find-files.outputs.found }} + + - name: Log + run: | + cat "${{ steps.yaml-lint.outputs.logfile }}" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 8ea3744..cab683a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,40 +1,39 @@ -# Object files +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo *.o -*.ko *.obj -*.elf # Precompiled Headers *.gch *.pch -# Libraries -*.lib -*.a -*.la -*.lo - -# Shared objects (inc. Windows DLLs) -*.dll +# Compiled Dynamic libraries *.so -*.so.* *.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib # Executables *.exe *.out *.app -*.i*86 -*.x86_64 -*.hex - -# Debug files -*.dSYM/ -*.su -# Binary -example -example.exe +# JetBrains IDE +.idea/ -.vscode/ -build/ \ No newline at end of file +# build directories +build/ +cmake-*/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..6cbf29c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "third-party/googletest"] + path = third-party/googletest + url = https://github.com/google/googletest.git + branch = v1.14.x diff --git a/CMakeLists.txt b/CMakeLists.txt index 8fecb9f..daba122 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,77 +1,90 @@ cmake_minimum_required(VERSION 3.13 FATAL_ERROR) # target_link_directories -project(tray C) +project(tray + LANGUAGES C + DESCRIPTION "A cross-platform system tray library") -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") +set(PROJECT_LICENSE "MIT") +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + message(STATUS "Setting build type to 'Release' as none was specified.") + set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) +endif() + +set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") + +# options +option(BUILD_TESTS "Build tests" ON) # Generate 'compile_commands.json' for clang_complete set(CMAKE_COLOR_MAKEFILE ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - find_package (PkgConfig REQUIRED) - -file(GLOB SRCS - ${CMAKE_CURRENT_LIST_DIR}/*.h - ${CMAKE_CURRENT_LIST_DIR}/*.ico - ${CMAKE_CURRENT_LIST_DIR}/*.png) +file(GLOB TRAY_SOURCES + "${CMAKE_SOURCE_DIR}/src/*.h" + "${CMAKE_SOURCE_DIR}/icons/*.ico" + "${CMAKE_SOURCE_DIR}/icons/*.png") if(WIN32) - list(APPEND SRCS ${CMAKE_CURRENT_SOURCE_DIR}/tray_windows.c) + list(APPEND TRAY_SOURCES "${CMAKE_SOURCE_DIR}/src/tray_windows.c") else() - if(UNIX) - if(APPLE) - find_library(COCOA Cocoa REQUIRED) - list(APPEND SRCS ${CMAKE_CURRENT_SOURCE_DIR}/tray_darwin.m) - else() - find_package(APPINDICATOR REQUIRED) - find_package(LIBNOTIFY REQUIRED) - list(APPEND SRCS ${CMAKE_CURRENT_SOURCE_DIR}/tray_linux.c) - endif() - endif() + if(UNIX) + if(APPLE) + find_library(COCOA Cocoa REQUIRED) + list(APPEND TRAY_SOURCES "${CMAKE_SOURCE_DIR}/src/tray_darwin.m") + else() + find_package(APPINDICATOR REQUIRED) + find_package(LIBNOTIFY REQUIRED) + list(APPEND TRAY_SOURCES "${CMAKE_SOURCE_DIR}/src/tray_linux.c") + endif() + endif() endif() -add_library(tray STATIC ${SRCS}) +add_library(tray STATIC ${TRAY_SOURCES}) set_property(TARGET tray PROPERTY C_STANDARD 99) if(WIN32) - target_compile_definitions(tray PRIVATE TRAY_WINAPI=1 WIN32_LEAN_AND_MEAN NOMINMAX) - if(MSVC) - target_compile_options(tray PRIVATE "/MT$<$:d>") - endif() + target_compile_definitions(tray PRIVATE TRAY_WINAPI=1 WIN32_LEAN_AND_MEAN NOMINMAX) + if(MSVC) + target_compile_options(tray PRIVATE "/MT$<$:d>") + endif() else() - if(UNIX) - if(APPLE) - target_compile_definitions(tray PRIVATE TRAY_APPKIT=1) - target_link_libraries(tray PRIVATE ${COCOA}) - else() - target_compile_options(tray PRIVATE ${APPINDICATOR_CFLAGS}) - target_link_directories(tray PRIVATE ${APPINDICATOR_LIBRARY_DIRS}) - target_compile_definitions(tray PRIVATE TRAY_APPINDICATOR=1) - if(APPINDICATOR_AYATANA) - target_compile_definitions(tray PRIVATE TRAY_AYATANA_APPINDICATOR=1) - endif() - if(APPINDICATOR_LEGACY) - target_compile_definitions(tray PRIVATE TRAY_LEGACY_APPINDICATOR=1) - endif() - target_compile_definitions(tray PRIVATE TRAY_LIBNOTIFY=1) - target_link_libraries(tray PRIVATE ${APPINDICATOR_LIBRARIES} ${LIBNOTIFY_LIBRARIES}) - endif() + if(UNIX) + if(APPLE) + target_compile_definitions(tray PRIVATE TRAY_APPKIT=1) + target_link_libraries(tray PRIVATE ${COCOA}) + else() + target_compile_options(tray PRIVATE ${APPINDICATOR_CFLAGS}) + target_link_directories(tray PRIVATE ${APPINDICATOR_LIBRARY_DIRS}) + target_compile_definitions(tray PRIVATE TRAY_APPINDICATOR=1) + if(APPINDICATOR_AYATANA) + target_compile_definitions(tray PRIVATE TRAY_AYATANA_APPINDICATOR=1) + endif() + if(APPINDICATOR_LEGACY) + target_compile_definitions(tray PRIVATE TRAY_LEGACY_APPINDICATOR=1) + endif() + target_compile_definitions(tray PRIVATE TRAY_LIBNOTIFY=1) + target_link_libraries(tray PRIVATE ${APPINDICATOR_LIBRARIES} ${LIBNOTIFY_LIBRARIES}) endif() + endif() endif() add_library(tray::tray ALIAS tray) -add_executable(tray_example ${CMAKE_CURRENT_SOURCE_DIR}/example.c) +add_executable(tray_example "${CMAKE_SOURCE_DIR}/src/example.c") target_link_libraries(tray_example tray::tray) -configure_file(${CMAKE_CURRENT_LIST_DIR}/icon.ico ${CMAKE_BINARY_DIR}/icon.ico COPYONLY) -configure_file(${CMAKE_CURRENT_LIST_DIR}/icon.png ${CMAKE_BINARY_DIR}/icon.png COPYONLY) +configure_file("${CMAKE_SOURCE_DIR}/icons/icon.ico" "${CMAKE_BINARY_DIR}/icon.ico" COPYONLY) +configure_file("${CMAKE_SOURCE_DIR}/icons/icon.png" "${CMAKE_BINARY_DIR}/icon.png" COPYONLY) INSTALL(TARGETS tray tray DESTINATION lib) IF(NOT WIN32) - INSTALL(FILES tray.h DESTINATION include) + INSTALL(FILES tray.h DESTINATION include) ENDIF() +# tests +if(BUILD_TESTS) + add_subdirectory(tests) +endif() diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..c9d3a1a --- /dev/null +++ b/codecov.yml @@ -0,0 +1,15 @@ +--- +codecov: + branch: master + +coverage: + status: + project: + default: + target: auto + threshold: 10% + +comment: + layout: "diff, flags, files" + behavior: default + require_changes: false # if true: only post the comment if coverage changes diff --git a/icon.ico b/icons/icon.ico similarity index 100% rename from icon.ico rename to icons/icon.ico diff --git a/icon.png b/icons/icon.png similarity index 100% rename from icon.png rename to icons/icon.png diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 0000000..2d2abbf --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1 @@ +clang-format diff --git a/scripts/update_clang_format.py b/scripts/update_clang_format.py new file mode 100644 index 0000000..d5a90ec --- /dev/null +++ b/scripts/update_clang_format.py @@ -0,0 +1,37 @@ +# standard imports +import os +import subprocess + +# variables +directories = [ + 'src', + 'tests', +] +file_types = [ + 'cpp', + 'h', + 'm', + 'mm' +] + + +def clang_format(file: str): + print(f'Formatting {file} ...') + subprocess.run(['clang-format', '-i', file]) + + +def main(): + """ + Main entry point. + """ + # walk the directories + for directory in directories: + for root, dirs, files in os.walk(directory): + for file in files: + file_path = os.path.join(root, file) + if os.path.isfile(file_path) and file.rsplit('.')[-1] in file_types: + clang_format(file=file_path) + + +if __name__ == '__main__': + main() diff --git a/example.c b/src/example.c similarity index 100% rename from example.c rename to src/example.c diff --git a/tray.h b/src/tray.h similarity index 100% rename from tray.h rename to src/tray.h diff --git a/tray_darwin.m b/src/tray_darwin.m similarity index 100% rename from tray_darwin.m rename to src/tray_darwin.m diff --git a/tray_linux.c b/src/tray_linux.c similarity index 100% rename from tray_linux.c rename to src/tray_linux.c diff --git a/tray_windows.c b/src/tray_windows.c similarity index 100% rename from tray_windows.c rename to src/tray_windows.c diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..291f3c1 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,46 @@ +cmake_minimum_required(VERSION 3.13) +# https://github.com/google/oss-policies-info/blob/main/foundational-cxx-support-matrix.md#foundational-c-support + +project(test_tray) + +include_directories("${CMAKE_SOURCE_DIR}") + +enable_testing() + +# Add GoogleTest directory to the project +set(GTEST_SOURCE_DIR "${CMAKE_SOURCE_DIR}/third-party/googletest") +set(INSTALL_GTEST OFF) +set(INSTALL_GMOCK OFF) +add_subdirectory("${GTEST_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/googletest") +include_directories("${GTEST_SOURCE_DIR}/googletest/include" "${GTEST_SOURCE_DIR}") + +# coverage +# https://gcovr.com/en/stable/guide/compiling.html#compiler-options +set(CMAKE_CXX_FLAGS "-fprofile-arcs -ftest-coverage -O1") +set(CMAKE_C_FLAGS "-fprofile-arcs -ftest-coverage -O1") + +# if windows +if (WIN32) + # For Windows: Prevent overriding the parent project's compiler/linker settings + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # cmake-lint: disable=C0103 +endif () + +file(GLOB_RECURSE TEST_SOURCES + ${CMAKE_SOURCE_DIR}/tests/conftest.cpp + ${CMAKE_SOURCE_DIR}/tests/utils.cpp + ${CMAKE_SOURCE_DIR}/tests/test_*.cpp) + +add_executable(${PROJECT_NAME} + ${TEST_SOURCES} + ${TRAY_SOURCES}) +set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 17) +target_link_libraries(${PROJECT_NAME} + ${TRAY_EXTERNAL_LIBRARIES} + gtest + gtest_main # if we use this we don't need our own main function + ${PLATFORM_LIBRARIES}) +target_compile_definitions(${PROJECT_NAME} PUBLIC ${TRAY_DEFINITIONS} ${TEST_DEFINITIONS}) +target_compile_options(${PROJECT_NAME} PRIVATE $<$:${TRAY_COMPILE_OPTIONS}>) +target_link_options(${PROJECT_NAME} PRIVATE) + +add_test(NAME ${PROJECT_NAME} COMMAND tray_test) diff --git a/tests/conftest.cpp b/tests/conftest.cpp new file mode 100644 index 0000000..079cd7f --- /dev/null +++ b/tests/conftest.cpp @@ -0,0 +1,173 @@ +#include +#include + +#include + +// Undefine the original TEST macro +#undef TEST + +// Redefine TEST to use our BaseTest class, to automatically use our BaseTest fixture +#define TEST(test_case_name, test_name) \ + GTEST_TEST_(test_case_name, test_name, ::BaseTest, \ + ::testing::internal::GetTypeId<::BaseTest>()) + +/** + * @brief Base class for tests. + * + * This class provides a base test fixture for all tests. + * + * ``cout``, ``stderr``, and ``stdout`` are redirected to a buffer, and the buffer is printed if the test fails. + * + * @todo Retain the color of the original output. + */ +class BaseTest: public ::testing::Test { +protected: + // https://stackoverflow.com/a/58369622/11214013 + + // we can possibly use some internal googletest functions to capture stdout and stderr, but I have not tested this + // https://stackoverflow.com/a/33186201/11214013 + + BaseTest(): + sbuf { nullptr }, pipe_stdout { nullptr }, pipe_stderr { nullptr } { + // intentionally empty + } + + ~BaseTest() override = default; + + void + SetUp() override { + // todo: only run this one time, instead of every time a test is run + // see: https://stackoverflow.com/questions/2435277/googletest-accessing-the-environment-from-a-test + // get command line args from the test executable + testArgs = ::testing::internal::GetArgvs(); + + // then get the directory of the test executable + // std::string path = ::testing::internal::GetArgvs()[0]; + testBinary = testArgs[0]; + + // get the directory of the test executable + testBinaryDir = std::filesystem::path(testBinary).parent_path(); + + // If testBinaryDir is empty or `.` then set it to the current directory + // maybe some better options here: https://stackoverflow.com/questions/875249/how-to-get-current-directory + if (testBinaryDir.empty() || testBinaryDir.string() == ".") { + testBinaryDir = std::filesystem::current_path(); + } + + sbuf = std::cout.rdbuf(); // save cout buffer (std::cout) + std::cout.rdbuf(cout_buffer.rdbuf()); // redirect cout to buffer (std::cout) + } + + void + TearDown() override { + std::cout.rdbuf(sbuf); // restore cout buffer + + // get test info + const ::testing::TestInfo *const test_info = ::testing::UnitTest::GetInstance()->current_test_info(); + + if (test_info->result()->Failed()) { + std::cout << std::endl + << "Test failed: " << test_info->name() << std::endl + << std::endl + << "Captured boost log:" << std::endl + << boost_log_buffer.str() << std::endl + << "Captured cout:" << std::endl + << cout_buffer.str() << std::endl + << "Captured stdout:" << std::endl + << stdout_buffer.str() << std::endl + << "Captured stderr:" << std::endl + << stderr_buffer.str() << std::endl; + } + + sbuf = nullptr; // clear sbuf + if (pipe_stdout) { + pclose(pipe_stdout); + pipe_stdout = nullptr; + } + if (pipe_stderr) { + pclose(pipe_stderr); + pipe_stderr = nullptr; + } + } + + // functions and variables + std::vector testArgs; // CLI arguments used + std::filesystem::path testBinary; // full path of this binary + std::filesystem::path testBinaryDir; // full directory of this binary + std::stringstream boost_log_buffer; // declare boost_log_buffer + std::stringstream cout_buffer; // declare cout_buffer + std::stringstream stdout_buffer; // declare stdout_buffer + std::stringstream stderr_buffer; // declare stderr_buffer + std::streambuf *sbuf; + FILE *pipe_stdout; + FILE *pipe_stderr; + + int + exec(const char *cmd) { + std::array buffer {}; + pipe_stdout = popen((std::string(cmd) + " 2>&1").c_str(), "r"); + pipe_stderr = popen((std::string(cmd) + " 2>&1").c_str(), "r"); + if (!pipe_stdout || !pipe_stderr) { + throw std::runtime_error("popen() failed!"); + } + while (fgets(buffer.data(), buffer.size(), pipe_stdout) != nullptr) { + stdout_buffer << buffer.data(); + } + while (fgets(buffer.data(), buffer.size(), pipe_stderr) != nullptr) { + stderr_buffer << buffer.data(); + } + int returnCode = pclose(pipe_stdout); + pipe_stdout = nullptr; + if (returnCode != 0) { + std::cout << "Error: " << stderr_buffer.str() << std::endl + << "Return code: " << returnCode << std::endl; + } + return returnCode; + } +}; + +class LinuxTest: public BaseTest { +protected: + void + SetUp() override { +#ifndef __linux__ + GTEST_SKIP_("Skipping, this test is for Linux only."); +#endif + } + + void + TearDown() override { + BaseTest::TearDown(); + } +}; + +class MacOSTest: public BaseTest { +protected: + void + SetUp() override { +#if !defined(__APPLE__) || !defined(__MACH__) + GTEST_SKIP_("Skipping, this test is for macOS only."); +#endif + } + + void + TearDown() override { + BaseTest::TearDown(); + } +}; + +class WindowsTest: public BaseTest { +protected: + void + SetUp() override { +#ifndef _WIN32 + GTEST_SKIP_("Skipping, this test is for Windows only."); +#endif + BaseTest::SetUp(); + } + + void + TearDown() override { + BaseTest::TearDown(); + } +}; diff --git a/tests/unit/test_tray.cpp b/tests/unit/test_tray.cpp new file mode 100644 index 0000000..a6ebfa1 --- /dev/null +++ b/tests/unit/test_tray.cpp @@ -0,0 +1,116 @@ +#include + +#if defined (_WIN32) || defined (_WIN64) +#define TRAY_WINAPI 1 +#elif defined (__linux__) || defined (linux) || defined (__linux) +#define TRAY_APPINDICATOR 1 +#elif defined (__APPLE__) || defined (__MACH__) +#define TRAY_APPKIT 1 +#endif + +#include "src/tray.h" + +#if TRAY_APPINDICATOR +#define TRAY_ICON1 "mail-message-new" +#define TRAY_ICON2 "mail-message-new" +#elif TRAY_APPKIT +#define TRAY_ICON1 "icon.png" +#define TRAY_ICON2 "icon.png" +#elif TRAY_WINAPI +#define TRAY_ICON1 "icon.ico" +#define TRAY_ICON2 "icon.ico" +#endif + +class TrayTest : public BaseTest { +protected: + static struct tray testTray; + + // Static arrays for submenus + static struct tray_menu submenu7_8[]; + static struct tray_menu submenu5_6[]; + static struct tray_menu submenu_second[]; + static struct tray_menu submenu[]; + + // Non-static member functions + static void hello_cb(struct tray_menu *item) { + // Mock implementation + } + static void toggle_cb(struct tray_menu *item) { + item->checked = !item->checked; + tray_update(&testTray); + } + static void quit_cb(struct tray_menu *item) { + tray_exit(); + } + static void submenu_cb(struct tray_menu *item) { + // Mock implementation + tray_update(&testTray); + } + + void SetUp() override { + testTray.icon = TRAY_ICON1; + testTray.tooltip = "TestTray"; + testTray.menu = submenu; + } + + void TearDown() override { + // Clean up any resources if needed + } +}; + +// Define the static arrays +struct tray_menu TrayTest::submenu7_8[] = { + {.text = "7", .cb = submenu_cb}, + {.text = "-"}, + {.text = "8", .cb = submenu_cb}, + {.text = nullptr} +}; +struct tray_menu TrayTest::submenu5_6[] = { + {.text = "5", .cb = submenu_cb}, + {.text = "6", .cb = submenu_cb}, + {.text = nullptr} +}; +struct tray_menu TrayTest::submenu_second[] = { + {.text = "THIRD", .submenu = submenu7_8}, + {.text = "FOUR", .submenu = submenu5_6}, + {.text = nullptr} +}; +struct tray_menu TrayTest::submenu[] = { + {.text = "Hello", .cb = hello_cb}, + {.text = "Checked", .checked = 1, .checkbox = 1, .cb = toggle_cb}, + {.text = "Disabled", .disabled = 1}, + {.text = "-"}, + {.text = "SubMenu", .submenu = submenu_second}, + {.text = "-"}, + {.text = "Quit", .cb = quit_cb}, + {.text = nullptr} +}; +struct tray TrayTest::testTray = { + .icon = TRAY_ICON1, + .tooltip = "TestTray", + .menu = submenu +}; + +TEST_F(TrayTest, TestTrayInit) { + int result = tray_init(&testTray); + // Check the result of tray_init + EXPECT_EQ(result, 0); +} + +TEST_F(TrayTest, TestTrayLoop) { + int result = tray_loop(1); + // Check the result of tray_loop + EXPECT_EQ(result, 0); +} + +TEST_F(TrayTest, TestTrayUpdate) { + tray_update(&testTray); + // Check the state of testTray after update + // This may require some additional setup in the testTray initialization +} + +TEST_F(TrayTest, TestTrayExit) { + tray_exit(); + // Check the state after tray_exit + // This may require some additional checks or setup +} diff --git a/tests/utils.cpp b/tests/utils.cpp new file mode 100644 index 0000000..2ff1bfd --- /dev/null +++ b/tests/utils.cpp @@ -0,0 +1,21 @@ +/** + * @file utils.cpp + * @brief Utility functions + */ + +#include "utils.h" + +/** + * @brief Set an environment variable. + * @param name Name of the environment variable + * @param value Value of the environment variable + * @return 0 on success, non-zero error code on failure + */ +int +setEnv(const std::string &name, const std::string &value) { +#ifdef _WIN32 + return _putenv_s(name.c_str(), value.c_str()); +#else + return setenv(name.c_str(), value.c_str(), 1); +#endif +} diff --git a/tests/utils.h b/tests/utils.h new file mode 100644 index 0000000..f82de91 --- /dev/null +++ b/tests/utils.h @@ -0,0 +1,11 @@ +/** + * @file utils.h + * @brief Reusable functions for tests. + */ + +#pragma once + +#include + +int +setEnv(const std::string &name, const std::string &value); diff --git a/third-party/googletest b/third-party/googletest new file mode 160000 index 0000000..f8d7d77 --- /dev/null +++ b/third-party/googletest @@ -0,0 +1 @@ +Subproject commit f8d7d77c06936315286eb55f8de22cd23c188571