mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-08 02:53:25 -08:00
Compare commits
22 commits
043c821b7c
...
41f01ff4a9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41f01ff4a9 | ||
|
|
ecb77e631e | ||
|
|
8c64c6fc9e | ||
|
|
e3f3d635d3 | ||
|
|
18cd13d196 | ||
|
|
3745c5d21d | ||
|
|
8572ac65e6 | ||
|
|
77a0253242 | ||
|
|
c08b345b71 | ||
|
|
1a4d89ff8f | ||
|
|
4feb31bd1a | ||
|
|
4c5a5e3e55 | ||
|
|
5d06bb903d | ||
|
|
57a6a0d9cb | ||
|
|
f83a61979f | ||
|
|
07eb9f0103 | ||
|
|
38acddeede | ||
|
|
74a6acde91 | ||
|
|
4a010bd17f | ||
|
|
315cbae345 | ||
|
|
bdda3dedfa | ||
|
|
8822ade49d |
74 changed files with 2102 additions and 718 deletions
|
|
@ -9,6 +9,7 @@ RUN apt-get update && \
|
|||
cmake \
|
||||
file \
|
||||
g++ \
|
||||
gcovr \
|
||||
git \
|
||||
libgl-dev \
|
||||
liblzma-dev \
|
||||
|
|
|
|||
|
|
@ -8,14 +8,17 @@
|
|||
# --suffix <suffix> renames package with this suffix, requires arg
|
||||
# --server compiles servatrice
|
||||
# --test runs tests
|
||||
# --sanitize builds with AddressSanitizer and UndefinedBehaviorSanitizer, gcc/clang only
|
||||
# --coverage instruments the test run for line coverage and prints a gcovr summary, gcc only
|
||||
|
||||
# --debug or --release sets the build type ie CMAKE_BUILD_TYPE
|
||||
# --ccache [<size>] uses ccache and shows stats, optionally provide size
|
||||
# --evict-ccache <age> runs ccache eviction based on given age after build
|
||||
# --dir <dir> sets the name of the build dir, default is "build"
|
||||
# --cmake-generator <generator> sets CMAKE_GENERATOR as used by cmake
|
||||
# --target-macos-version <version> sets the min os version - only used for macOS builds
|
||||
# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION
|
||||
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --ccache <ccache_size> --dir <dir>)
|
||||
# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION MAKE_SANITIZE MAKE_COVERAGE
|
||||
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --sanitize --coverage --ccache <ccache_size> --dir <dir>)
|
||||
# exitcode: 1 for failure, 3 for invalid arguments
|
||||
|
||||
# Read arguments
|
||||
|
|
@ -57,6 +60,14 @@ while [[ $# != 0 ]]; do
|
|||
MAKE_TEST=1
|
||||
shift
|
||||
;;
|
||||
'--sanitize')
|
||||
MAKE_SANITIZE=1
|
||||
shift
|
||||
;;
|
||||
'--coverage')
|
||||
MAKE_COVERAGE=1
|
||||
shift
|
||||
;;
|
||||
'--debug')
|
||||
BUILDTYPE="Debug"
|
||||
shift
|
||||
|
|
@ -131,6 +142,15 @@ if [[ ! $BUILD_DIR ]]; then
|
|||
BUILD_DIR="build"
|
||||
fi
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
if [[ $MAKE_COVERAGE ]]; then
|
||||
# Resolved while the working directory is still the source root. $0 is relative to the
|
||||
# directory the script was invoked from, so after the cd below it no longer resolves, and
|
||||
# under `set -e` the failed subshell would abort the build rather than just yielding an empty
|
||||
# variable. Only the coverage report needs this, so only coverage pays for it.
|
||||
SRC_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
fi
|
||||
|
||||
cd "$BUILD_DIR"
|
||||
|
||||
# Set minimum CMake Version
|
||||
|
|
@ -147,6 +167,17 @@ fi
|
|||
if [[ $MAKE_TEST ]]; then
|
||||
flags+=("-DTEST=1")
|
||||
fi
|
||||
if [[ $MAKE_SANITIZE ]]; then
|
||||
flags+=("-DENABLE_SANITIZERS=ON")
|
||||
else
|
||||
# The release-package step re-runs cmake in the same build dir, so an
|
||||
# ENABLE_SANITIZERS=ON left over from the debug step would otherwise leak
|
||||
# into the shipped binaries (libasan/libubsan Depends in the DEB).
|
||||
flags+=("-DENABLE_SANITIZERS=OFF")
|
||||
fi
|
||||
if [[ $MAKE_COVERAGE ]]; then
|
||||
flags+=("-DENABLE_COVERAGE=ON")
|
||||
fi
|
||||
if [[ $USE_CCACHE ]]; then
|
||||
flags+=("-DUSE_CCACHE=1")
|
||||
# PCH-aware caching is required or ccache refuses to cache any TU that
|
||||
|
|
@ -306,7 +337,50 @@ fi
|
|||
|
||||
if [[ $MAKE_TEST ]]; then
|
||||
echo "::group::Run tests"
|
||||
ctest -C "$BUILDTYPE" --output-on-failure
|
||||
if [[ $MAKE_SANITIZE ]]; then
|
||||
# detect_leaks=0: Qt and the sanitizers' own runtimes both leak at exit by design, and
|
||||
# LeakSanitizer's default is aggressive enough to bury our real findings under hundreds
|
||||
# of process-teardown reports. Malloc/free misuse and use-after-free are what ASan is for
|
||||
# and they stay fully on.
|
||||
# abort_on_error=1: a bad access leaves the process in an unusable state, so continuing
|
||||
# only produces a second, more confusing report from the wreckage.
|
||||
# UBSAN halt_on_error=1: without it UBSan prints "runtime error: ..." but the
|
||||
# process exits 0 (recover mode), so gtest/ctest report a pass and the job
|
||||
# stays green. Combined with -fno-sanitize-recover=undefined at compile time,
|
||||
# the first finding aborts the test process with a non-zero exit.
|
||||
export ASAN_OPTIONS="detect_leaks=0:abort_on_error=1:print_stacktrace=1:symbolize=1"
|
||||
export UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1:symbolize=1"
|
||||
fi
|
||||
# Every TEST() is its own CTest entry, so the suite is process-spawn bound rather than CPU
|
||||
# bound and -j recovers nearly all of the per-process overhead. The test binaries are safe to
|
||||
# run concurrently: the only one that claims a fixed resource is single_instance_manager_test,
|
||||
# which already scopes its local socket name to its own pid so parallel copies cannot collide.
|
||||
ctest -C "$BUILDTYPE" --output-on-failure -j "${CTEST_JOBS:-4}"
|
||||
echo "::endgroup::"
|
||||
fi
|
||||
|
||||
if [[ $MAKE_COVERAGE ]]; then
|
||||
# gcovr is not a build dependency and is not available everywhere, so a missing one is reported
|
||||
# and skipped rather than failing an otherwise green build.
|
||||
echo "::group::Coverage report"
|
||||
GCOVR="$(command -v gcovr || true)"
|
||||
if [[ -z $GCOVR ]]; then
|
||||
echo "gcovr not found; install it to produce a coverage report"
|
||||
else
|
||||
"$GCOVR" --version
|
||||
# This script configures with `cmake ..`, so the build dir it was started from is the object
|
||||
# directory and the source root is one level up. Both are passed explicitly: gcovr otherwise
|
||||
# walks the current directory and trips over in-tree build directories left by other tooling,
|
||||
# whose stale .gcno files cannot be resolved. --config is named too, so the filters apply no
|
||||
# matter what directory gcovr happens to start from.
|
||||
GCOVR_ARGS=("." --root "$SRC_ROOT" --object-directory "." --config "$SRC_ROOT/gcovr.cfg")
|
||||
"$GCOVR" "${GCOVR_ARGS[@]}" --txt --decisions
|
||||
"$GCOVR" "${GCOVR_ARGS[@]}" --html-details coverage-details.html --html-title "Cockatrice line coverage"
|
||||
"$GCOVR" "${GCOVR_ARGS[@]}" --lcov -o coverage.lcov
|
||||
"$GCOVR" "${GCOVR_ARGS[@]}" --cobertura-pretty -o coverage.xml
|
||||
"$GCOVR" "${GCOVR_ARGS[@]}" --json-summary-pretty -o coverage-summary.json
|
||||
echo "Coverage artifacts written to coverage-details.html, coverage.lcov, coverage.xml and coverage-summary.json"
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -13,11 +13,17 @@
|
|||
# --set-cache <location> sets the location to cache the image or for ccache
|
||||
#
|
||||
# requires: docker
|
||||
# uses env: NAME CACHE BUILD GET SAVE INTERACTIVE
|
||||
# uses env: NAME IMAGE_SUFFIX CACHE BUILD GET SAVE INTERACTIVE
|
||||
# (correspond to args: <name> --set-cache <cache> --build --get --save --interactive)
|
||||
# sets env: RUN CCACHE_DIR IMAGE_NAME RUN_ARGS RUN_OPTS BUILD_SCRIPT
|
||||
# exitcode: 1 for failure, 2 for missing dockerfile, 3 for invalid arguments
|
||||
#
|
||||
# IMAGE_SUFFIX is optional and only appended to the image name, never to NAME, so that a matrix
|
||||
# leg can have its own image and image cache without needing its own directory under .ci. Two legs
|
||||
# building and tagging the same image concurrently would race on `docker build --tag`.
|
||||
# It is deliberately not called SUFFIX: compile.sh already uses SUFFIX for output file names, and
|
||||
# a job-level SUFFIX would be silently overridden by those per-step env blocks.
|
||||
#
|
||||
# exported RUN function will run the BUILD_SCRIPT inside of the docker container.
|
||||
# note that the docker container will not inherit any environment variables!
|
||||
#
|
||||
|
|
@ -81,7 +87,7 @@ if ! [[ $NAME ]]; then
|
|||
return 3
|
||||
fi
|
||||
|
||||
export IMAGE_NAME="${project_name,,}_${NAME,,}" # lower case
|
||||
export IMAGE_NAME="${project_name,,}_${NAME,,}${IMAGE_SUFFIX:+_${IMAGE_SUFFIX,,}}" # lower case
|
||||
|
||||
docker_dir=".ci/$NAME"
|
||||
if ! [[ -r $docker_dir/Dockerfile ]]; then
|
||||
|
|
|
|||
56
.github/workflows/desktop-build.yml
vendored
56
.github/workflows/desktop-build.yml
vendored
|
|
@ -144,17 +144,36 @@ jobs:
|
|||
|
||||
package: DEB
|
||||
|
||||
name: ${{ matrix.distro }} ${{ matrix.version }}
|
||||
# The one leg that runs the suite under AddressSanitizer and
|
||||
# UndefinedBehaviorSanitizer. Debian 13 and Fedora 44 keep covering the
|
||||
# uninstrumented suite, so nothing is lost by instrumenting this one.
|
||||
sanitize: yes
|
||||
|
||||
- distro: Ubuntu
|
||||
version: 26.04
|
||||
variant: cover # separate image and ccache, same distro
|
||||
|
||||
# The one leg that runs instrumented for line coverage and publishes the report as
|
||||
# a workflow artifact. It is a separate leg rather than a second flag on the
|
||||
# sanitizer leg because the two instruments answer different questions: the
|
||||
# sanitizer leg reports what the run did wrong, the coverage leg reports what the
|
||||
# run never touched at all. Combining them would also slow both down. Packaging is
|
||||
# skipped because the leg above already publishes the DEB for this distro.
|
||||
coverage: yes
|
||||
package: skip
|
||||
|
||||
name: ${{ matrix.distro }} ${{ matrix.version }}${{ matrix.variant && format(' {0}', matrix.variant) || '' }}
|
||||
needs: configure
|
||||
runs-on: ubuntu-latest # https://github.com/actions/runner-images
|
||||
continue-on-error: ${{ matrix.allow-failure == 'yes' }}
|
||||
timeout-minutes: 70
|
||||
env:
|
||||
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache
|
||||
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }} # directory for caching docker image and ccache
|
||||
CCACHE_EVICTION_AGE: 7d
|
||||
CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
||||
CMAKE_GENERATOR: 'Ninja'
|
||||
NAME: ${{ matrix.distro }}${{ matrix.version }}
|
||||
IMAGE_SUFFIX: ${{ matrix.variant }}
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
|
|
@ -164,7 +183,7 @@ jobs:
|
|||
id: restore_ccache
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
key: ccache-${{ matrix.distro }}${{ matrix.version }}
|
||||
key: ccache-${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }}
|
||||
path: ${{ env.CACHE }}
|
||||
|
||||
- name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image"
|
||||
|
|
@ -174,6 +193,9 @@ jobs:
|
|||
- name: "Build debug and test"
|
||||
if: matrix.test != 'skip'
|
||||
shell: bash
|
||||
env:
|
||||
sanitize: '${{ matrix.sanitize }}'
|
||||
coverage: '${{ matrix.coverage }}'
|
||||
run: |
|
||||
source .ci/docker.sh
|
||||
args=()
|
||||
|
|
@ -181,7 +203,33 @@ jobs:
|
|||
args+=(--ccache "$CCACHE_SIZE")
|
||||
args+=(--cmake-generator "$CMAKE_GENERATOR")
|
||||
|
||||
RUN --server --debug --test "${args[@]}"
|
||||
extra=()
|
||||
[[ $sanitize == yes ]] && extra+=(--sanitize)
|
||||
[[ $coverage == yes ]] && extra+=(--coverage)
|
||||
|
||||
RUN --server --debug --test "${extra[@]}" "${args[@]}"
|
||||
|
||||
# RUN bind-mounts the workspace into the container, so the report the build wrote into
|
||||
# build/ is already on the host and no second container is needed to collect it.
|
||||
- name: "Collect coverage report"
|
||||
if: matrix.coverage == 'yes'
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p coverage
|
||||
# --html-details writes one page per source file plus a stylesheet and script, and the
|
||||
# index links to all of them by relative name. Copying the index alone would publish a
|
||||
# report where every link 404s, so the whole family has to travel together.
|
||||
cp build/coverage-details* coverage/
|
||||
cp build/coverage.lcov build/coverage.xml build/coverage-summary.json coverage/
|
||||
ls -la coverage/
|
||||
|
||||
- name: "Upload coverage artifact"
|
||||
if: matrix.coverage == 'yes'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage/
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: "Build release package"
|
||||
id: build
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ option(WITH_ORACLE "Build Cockatrice card database tool (Oracle)" ON)
|
|||
option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF)
|
||||
# Compile tests
|
||||
option(TEST "Build tests" OFF)
|
||||
# Build with AddressSanitizer and UndefinedBehaviorSanitizer
|
||||
option(ENABLE_SANITIZERS "Build with AddressSanitizer and UndefinedBehaviorSanitizer" OFF)
|
||||
# Build instrumented for GCC line coverage, reported with gcovr
|
||||
option(ENABLE_COVERAGE "Build instrumented for GCC line coverage" OFF)
|
||||
# Use vcpkg regardless of OS
|
||||
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
|
||||
|
||||
|
|
@ -211,6 +215,12 @@ else()
|
|||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
|
||||
endif()
|
||||
|
||||
# Always included, because they define cockatrice_disable_sanitizers_for() and
|
||||
# cockatrice_disable_coverage_for() for the vendored trees. They only touch the global flags
|
||||
# when ENABLE_SANITIZERS or ENABLE_COVERAGE is on.
|
||||
include(Sanitizers)
|
||||
include(Coverage)
|
||||
|
||||
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
add_definitions("-DSFMT_MEXP=19937")
|
||||
|
|
|
|||
75
cmake/Coverage.cmake
Normal file
75
cmake/Coverage.cmake
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Line-coverage instrumentation opt-in, plus per-source opt-out
|
||||
# Input: ENABLE_COVERAGE
|
||||
# Defines: cockatrice_disable_coverage_for
|
||||
#
|
||||
# Included once from the root CMakeLists.txt, immediately after the per-compiler flag
|
||||
# block, so add_compile_options() reaches every library and executable defined below.
|
||||
#
|
||||
# Note the name deliberately shadows CMake's own built-in Modules/Coverage.cmake. It resolves
|
||||
# to this file only because the root CMakeLists.txt puts cmake/ at the front of
|
||||
# CMAKE_MODULE_PATH; if that ever moves, include(Coverage) would silently pick up the built-in
|
||||
# module, which only defines a `COVERAGE` custom target and touches no global flags, so
|
||||
# instrumentation would quietly stop happening rather than erroring.
|
||||
|
||||
include(CheckCXXSourceCompiles)
|
||||
|
||||
# Opt a set of vendored sources out of instrumentation.
|
||||
#
|
||||
# cockatrice_disable_coverage_for(<source> ...)
|
||||
#
|
||||
# Third-party code drags the total down with lines no test can ever reach, and a total that
|
||||
# cannot move is not a useful signal. This is the same carve-out the sanitizer build uses.
|
||||
#
|
||||
# The sources must be named relative to the directory that compiles them. CMake resolves a
|
||||
# source-file property against the directory scope the target was declared in, so calling
|
||||
# this from the root with absolute paths silently does nothing. That is why the call sites
|
||||
# live next to the add_library/add_executable that pull the vendored code in.
|
||||
#
|
||||
# SKIP_PRECOMPILE_HEADERS mirrors the sanitizer helper. The precompiled header carries the
|
||||
# instrumentation flags, and mixing an instrumented PCH with an uninstrumented translation
|
||||
# unit makes GCC reject it.
|
||||
function(cockatrice_disable_coverage_for)
|
||||
if(NOT ENABLE_COVERAGE)
|
||||
return()
|
||||
endif()
|
||||
# There is no -fno-coverage: --coverage is shorthand for -fprofile-arcs -ftest-coverage, so both
|
||||
# halves have to be negated. CC rejects "-fno-coverage" outright, which is how the vendored C
|
||||
# files below hit it first.
|
||||
set_source_files_properties(
|
||||
${ARGN} PROPERTIES COMPILE_OPTIONS "-fno-profile-arcs;-fno-test-coverage" SKIP_PRECOMPILE_HEADERS ON
|
||||
)
|
||||
endfunction()
|
||||
|
||||
if(ENABLE_COVERAGE)
|
||||
# Line coverage needs the compiler's own toolchain, so only GCC is supported. Clang's
|
||||
# equivalent is llvm-cov, which reads a different intermediate format and would need a
|
||||
# separate report path; rejecting it is better than emitting .gcno files that gcovr cannot
|
||||
# read.
|
||||
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
message(FATAL_ERROR "ENABLE_COVERAGE requires GCC, but the compiler is ${CMAKE_CXX_COMPILER_ID}. "
|
||||
"Clang coverage needs llvm-cov and is not wired up."
|
||||
)
|
||||
endif()
|
||||
|
||||
# Probe the way the flag will actually be used: compile AND link. A .gcno file comes from the
|
||||
# compile step, but the runtime that writes .gcda files comes from the link step, so probing
|
||||
# only the compile step would pass on a toolchain that cannot actually produce coverage.
|
||||
set(_cockatrice_saved_required_flags "${CMAKE_REQUIRED_FLAGS}")
|
||||
set(_cockatrice_saved_required_link_options "${CMAKE_REQUIRED_LINK_OPTIONS}")
|
||||
set(CMAKE_REQUIRED_FLAGS "-O0 -g")
|
||||
set(CMAKE_REQUIRED_LINK_OPTIONS -fprofile-arcs -ftest-coverage)
|
||||
check_cxx_source_compiles("int main() { return 0; }" COCKATRICE_HAVE_COVERAGE_FLAGS)
|
||||
set(CMAKE_REQUIRED_FLAGS "${_cockatrice_saved_required_flags}")
|
||||
set(CMAKE_REQUIRED_LINK_OPTIONS "${_cockatrice_saved_required_link_options}")
|
||||
|
||||
if(NOT COCKATRICE_HAVE_COVERAGE_FLAGS)
|
||||
message(FATAL_ERROR "ENABLE_COVERAGE is on, but this toolchain rejected the coverage flags.")
|
||||
endif()
|
||||
|
||||
# -O0 keeps the line mapping honest: at higher optimisation levels the compiler merges and
|
||||
# relocates lines, and the report describes code that no longer exists in that form.
|
||||
add_compile_options(--coverage -O0 -g)
|
||||
add_link_options(--coverage)
|
||||
|
||||
message(STATUS "Coverage instrumentation enabled (--coverage). Report with gcovr over the build dir.")
|
||||
endif()
|
||||
|
|
@ -40,7 +40,9 @@ endif()
|
|||
if(TEST)
|
||||
# Union of Qt modules required across all test targets (independent of application targets).
|
||||
# When adding a new test that needs additional Qt modules, add them here rather than in the test's CMakeLists.txt.
|
||||
set(_TEST_NEEDED Concurrent Network Svg Widgets)
|
||||
# Test is QtTest: it brings QAbstractItemModelTester and QSignalSpy, which are how the Qt model
|
||||
# and signal contracts get verified without standing up a GUI.
|
||||
set(_TEST_NEEDED Concurrent Network Svg Widgets Test)
|
||||
endif()
|
||||
|
||||
set(REQUIRED_QT_COMPONENTS ${REQUIRED_QT_COMPONENTS} ${_SERVATRICE_NEEDED} ${_COCKATRICE_NEEDED} ${_ORACLE_NEEDED}
|
||||
|
|
|
|||
|
|
@ -304,10 +304,11 @@ SetShellVarContext all
|
|||
; _?=$INSTDIR keeps the old uninstaller running in place. An NSIS uninstaller
|
||||
; normally copies itself to %TEMP% and runs from there so that it can delete its
|
||||
; own directory, and the process we started exits at once. ExecWait then returns
|
||||
; before the uninstall has run, and the temp copy's "RMDir /r $INSTDIR" executes
|
||||
; while the section below is still writing the new payload. _?= disables the copy
|
||||
; and makes ExecWait block until the uninstall finishes. It must be the last
|
||||
; argument and must not be quoted, even when the path contains spaces.
|
||||
; before the uninstall has run, and the temp copy keeps deleting files - from
|
||||
; 3.1.0 and 3.1.1 on disk a blanket "RMDir /r $INSTDIR" - while the section
|
||||
; below is still writing the new payload. _?= disables the copy and makes
|
||||
; ExecWait block until the uninstall finishes. It must be the last argument
|
||||
; and must not be quoted, even when the path contains spaces.
|
||||
;
|
||||
; --- 32-bit uninstall ---
|
||||
SetRegView 32
|
||||
|
|
@ -1107,9 +1108,14 @@ ${AndIf} ${FileExists} "$INSTDIR\portable.dat"
|
|||
; upgrade portable mode
|
||||
!insertmacro CockLog "section: portable upgrade purge"
|
||||
!insertmacro CockLogListing "before-portable-purge" "*.dll"
|
||||
; plugins and translations are payload and are rewritten by the payload
|
||||
; below, so removing them here is safe. sounds and themes are payload
|
||||
; too - the default packs are rewritten below as well - but a portable
|
||||
; user's custom sounds and themes live in exactly those folders, so a
|
||||
; recursive purge here would delete user content along with the defaults.
|
||||
; Stale default files from an older release are overwritten or replaced
|
||||
; by the payload, never by a purge.
|
||||
RMDir /r "$INSTDIR\plugins"
|
||||
RMDir /r "$INSTDIR\sounds"
|
||||
RMDir /r "$INSTDIR\themes"
|
||||
RMDir /r "$INSTDIR\translations"
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
Delete "$INSTDIR\cockatrice.exe"
|
||||
|
|
@ -1263,16 +1269,32 @@ Section "un.Application" UnSecApplication
|
|||
SetShellVarContext all
|
||||
StrCpy $R1 0
|
||||
|
||||
; Record whether this was a portable install before the directory (and the
|
||||
; portable.dat marker inside it) is removed below.
|
||||
; Record whether this was a portable install before portable.dat is
|
||||
; removed below.
|
||||
IfFileExists "$INSTDIR\portable.dat" 0 +2
|
||||
StrCpy $R1 1
|
||||
|
||||
; Remove the entire application directory so any file that is not part of
|
||||
; the installed payload (e.g. build-tree artifacts such as *.dir folders,
|
||||
; *_autogen and *.tlog files from a build) cannot survive between an
|
||||
; uninstall and a fresh reinstall.
|
||||
RMDir /r "$INSTDIR"
|
||||
; Remove only what the installer wrote. The two lists below are generated
|
||||
; by CPack from the staged install tree: one Delete per installed file and
|
||||
; one non-recursive RMDir per installed directory, children first.
|
||||
; Anything the user put in $INSTDIR - decks, pictures, portable data
|
||||
; folders - is not on those lists and survives, and $INSTDIR itself is
|
||||
; only removed when nothing else is left in it. An earlier version of this
|
||||
; section ran a blanket "RMDir /r $INSTDIR" to keep build-tree artifacts
|
||||
; out of a fresh reinstall; that also deleted user decks whenever a user
|
||||
; had pointed their data folders into the install directory, so payload
|
||||
; purity may never be bought at that price again.
|
||||
@CPACK_NSIS_DELETE_FILES@
|
||||
@CPACK_NSIS_DELETE_DIRECTORIES@
|
||||
|
||||
; Written by the installer itself, so absent from the staged payload
|
||||
; above; qdebug.txt is app-generated log output next to the executables.
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
Delete "$INSTDIR\portable.dat"
|
||||
Delete "$INSTDIR\qdebug.txt"
|
||||
|
||||
; Succeeds only when no foreign files remain.
|
||||
RMDir "$INSTDIR"
|
||||
|
||||
RMDir /r "$SMPROGRAMS\Cockatrice"
|
||||
|
||||
|
|
|
|||
109
cmake/Sanitizers.cmake
Normal file
109
cmake/Sanitizers.cmake
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# AddressSanitizer + UndefinedBehaviorSanitizer opt-in, plus per-source opt-out
|
||||
# Input: ENABLE_SANITIZERS
|
||||
# Defines: cockatrice_disable_sanitizers_for
|
||||
#
|
||||
# Included once from the root CMakeLists.txt, immediately after the per-compiler flag
|
||||
# block, so add_compile_options() reaches every library and executable defined below.
|
||||
|
||||
include(CheckCXXSourceCompiles)
|
||||
|
||||
# Opt a set of vendored sources out of instrumentation.
|
||||
#
|
||||
# cockatrice_disable_sanitizers_for(<source> ...)
|
||||
#
|
||||
# Sanitizer findings inside vendored code are not actionable for us, and letting them
|
||||
# through buries our own bugs in noise until everyone learns to skip the job.
|
||||
#
|
||||
# The sources must be named relative to the directory that compiles them. CMake resolves a
|
||||
# source-file property against the directory scope the target was declared in, so calling
|
||||
# this from the root with absolute paths silently does nothing. That is why the call sites
|
||||
# live next to the add_library/add_executable that pull the vendored code in.
|
||||
#
|
||||
# SKIP_PRECOMPILE_HEADERS is not optional. The precompiled header is built with the
|
||||
# sanitizer flags, so it defines __SANITIZE_ADDRESS__; a translation unit compiled with
|
||||
# -fno-sanitize=all does not, and GCC then refuses the PCH with
|
||||
# "not used because __SANITIZE_ADDRESS__ not defined", which is a fatal -Werror=invalid-pch
|
||||
# in the servatrice and oracle builds.
|
||||
function(cockatrice_disable_sanitizers_for)
|
||||
if(NOT ENABLE_SANITIZERS)
|
||||
return()
|
||||
endif()
|
||||
set_source_files_properties(${ARGN} PROPERTIES COMPILE_OPTIONS "-fno-sanitize=all" SKIP_PRECOMPILE_HEADERS ON)
|
||||
endfunction()
|
||||
|
||||
if(ENABLE_SANITIZERS)
|
||||
# Probe a flag the way it will actually be used: on the compile step AND the link step.
|
||||
# check_cxx_compiler_flag() only probes the compile step, which makes every sanitizer
|
||||
# flag look unsupported because linking an ASan object without -fsanitize=address leaves
|
||||
# __asan_init undefined. That is a silent misconfiguration rather than an error, so the
|
||||
# probe has to link.
|
||||
function(cockatrice_probe_flag FLAG VARNAME)
|
||||
set(CMAKE_REQUIRED_FLAGS "${FLAG}")
|
||||
set(CMAKE_REQUIRED_LINK_OPTIONS "${FLAG}")
|
||||
check_cxx_source_compiles("int main() { return 0; }" ${VARNAME})
|
||||
unset(CMAKE_REQUIRED_FLAGS)
|
||||
unset(CMAKE_REQUIRED_LINK_OPTIONS)
|
||||
endfunction()
|
||||
|
||||
set(SANITIZER_FLAGS
|
||||
-fsanitize=address,undefined
|
||||
-fno-omit-frame-pointer
|
||||
# UBSan defaults to recover mode: it prints "runtime error: ..." but the
|
||||
# process exits 0, so gtest/ctest report a pass and CI stays green.
|
||||
# -fno-sanitize-recover makes the first finding abort instead. This is
|
||||
# probed like the other cosmetic flags, but CI also sets
|
||||
# UBSAN_OPTIONS=halt_on_error=1 as the runtime-side guarantee.
|
||||
-fno-sanitize-recover=undefined
|
||||
# Qt builds QObject subclasses into raw storage and writes the vptr afterwards, which
|
||||
# the vptr check reports on essentially every QObject instantiation. The check has no
|
||||
# value for us because nothing outside moc-generated code inherits QObject, so it is
|
||||
# off outright rather than suppressed piecemeal.
|
||||
-fno-sanitize=vptr
|
||||
)
|
||||
|
||||
# MSVC has no ASan/UBSan equivalent, and clang-cl would need /fsanitize=address plus a
|
||||
# matching runtime link, which is a separate piece of work.
|
||||
if(MSVC)
|
||||
message(FATAL_ERROR "ENABLE_SANITIZERS is not supported with MSVC")
|
||||
endif()
|
||||
|
||||
# Probe rather than assume, so an older toolchain gets the sanitizers it can support
|
||||
# instead of failing the build on a flag it does not recognize.
|
||||
set(SANITIZER_SUPPORTED_FLAGS)
|
||||
foreach(FLAG ${SANITIZER_FLAGS})
|
||||
string(MAKE_C_IDENTIFIER "COCKATRICE_HAS${FLAG}" _FLAG_VAR)
|
||||
cockatrice_probe_flag("${FLAG}" ${_FLAG_VAR})
|
||||
if(${_FLAG_VAR})
|
||||
list(APPEND SANITIZER_SUPPORTED_FLAGS ${FLAG})
|
||||
else()
|
||||
message(STATUS "Sanitizers: ${FLAG} unsupported by ${CMAKE_CXX_COMPILER_ID}, skipping")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# The whole point of the option is the sanitizers, so an unsupported pair is fatal even
|
||||
# though the cosmetic flags above are allowed to degrade.
|
||||
if(NOT "-fsanitize=address,undefined" IN_LIST SANITIZER_SUPPORTED_FLAGS)
|
||||
message(FATAL_ERROR "ENABLE_SANITIZERS requires -fsanitize=address,undefined, which "
|
||||
"${CMAKE_CXX_COMPILER_ID} does not accept"
|
||||
)
|
||||
endif()
|
||||
|
||||
add_compile_options(${SANITIZER_SUPPORTED_FLAGS})
|
||||
add_link_options(-fsanitize=address,undefined)
|
||||
|
||||
# Recorded so the test tree can label its entries and a plain `make` at the same time
|
||||
# cannot quietly disagree with what CI ran.
|
||||
set(COCKATRICE_SANITIZERS
|
||||
ON
|
||||
CACHE INTERNAL "AddressSanitizer and UBSan are enabled"
|
||||
)
|
||||
else()
|
||||
# ENABLE_SANITIZERS is OFF, but COCKATRICE_SANITIZERS is CACHE INTERNAL, so a
|
||||
# reused build dir (e.g. CI's release-package step reconfiguring the same dir
|
||||
# after the sanitized debug step) would otherwise keep skipping NO_SANITIZE
|
||||
# tests and keep the 300s discovery timeout. Reset it explicitly.
|
||||
set(COCKATRICE_SANITIZERS
|
||||
OFF
|
||||
CACHE INTERNAL "AddressSanitizer and UBSan are enabled" FORCE
|
||||
)
|
||||
endif()
|
||||
|
|
@ -3,22 +3,53 @@
|
|||
*
|
||||
* Safe for every target that links Qt Core, including the headless
|
||||
* Servatrice binary. Keep this header free of any widget/gui types.
|
||||
*
|
||||
* Candidates are chosen from IWYU include statistics: a header earns a
|
||||
* spot when it is not already reachable from the rest of this list and
|
||||
* enough translation units re-parse it. Qt headers are stable across
|
||||
* builds, so the precompiled header rarely invalidates.
|
||||
*/
|
||||
|
||||
#include <QBasicTimer>
|
||||
#include <QByteArray>
|
||||
#include <QCommandLineParser>
|
||||
#include <QCoreApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QHash>
|
||||
#include <QItemSelectionModel>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
#include <QMetaEnum>
|
||||
#include <QMetaObject>
|
||||
#include <QMutex>
|
||||
#include <QMutexLocker>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QRandomGenerator>
|
||||
#include <QReadWriteLock>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
#include <QSettings>
|
||||
#include <QSharedPointer>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QTemporaryDir>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <QUuid>
|
||||
#include <QVariant>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QXmlStreamWriter>
|
||||
|
|
|
|||
|
|
@ -4,27 +4,56 @@
|
|||
* Includes the Qt Core precompiled header plus the heavy Gui, Widgets and
|
||||
* Network layers that virtually every client translation unit re-parses.
|
||||
* Do not use on Servatrice (headless, QT_DONT_USE_QTGUI).
|
||||
*
|
||||
* Candidates are chosen from IWYU include statistics: a header earns a
|
||||
* spot when it is not already reachable from the rest of this list and
|
||||
* enough translation units re-parse it. Qt headers are stable across
|
||||
* builds, so the precompiled header rarely invalidates.
|
||||
*/
|
||||
|
||||
#include "qtcore_pch.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QCompleter>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDockWidget>
|
||||
#include <QFileDialog>
|
||||
#include <QFormLayout>
|
||||
#include <QFrame>
|
||||
#include <QGraphicsItem>
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QGraphicsView>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QImage>
|
||||
#include <QImageReader>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
#include <QLayout>
|
||||
#include <QListWidget>
|
||||
#include <QMainWindow>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QPainter>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
#include <QSpinBox>
|
||||
#include <QSplitter>
|
||||
#include <QStandardItemModel>
|
||||
#include <QTabWidget>
|
||||
#include <QTableWidget>
|
||||
#include <QTableWidgetItem>
|
||||
#include <QToolBar>
|
||||
#include <QToolButton>
|
||||
#include <QTreeWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
|
|
|||
|
|
@ -53,12 +53,13 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp
|
||||
src/interface/widgets/dialogs/dlg_share_deck.cpp
|
||||
src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp
|
||||
src/interface/widgets/dialogs/dlg_settings.cpp
|
||||
src/interface/widgets/dialogs/dlg_startup_card_check.cpp
|
||||
src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp
|
||||
src/interface/widgets/dialogs/dlg_update.cpp
|
||||
src/interface/widgets/dialogs/dlg_view_log.cpp
|
||||
src/interface/widgets/dialogs/override_printing_warning.cpp
|
||||
src/interface/widgets/dialogs/path_safety_warning.cpp
|
||||
src/interface/widgets/dialogs/settings_view.cpp
|
||||
src/interface/widgets/dialogs/tip_of_the_day.cpp
|
||||
src/interface/widgets/deck_share/deck_share_utils.cpp
|
||||
src/interface/widgets/deck_share/shared_deck_preview_widget.cpp
|
||||
|
|
@ -406,6 +407,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/tabs/tab_replays.cpp
|
||||
src/interface/widgets/tabs/tab_room.cpp
|
||||
src/interface/widgets/tabs/tab_server.cpp
|
||||
src/interface/widgets/tabs/tab_settings.cpp
|
||||
src/interface/widgets/tabs/tab_supervisor.cpp
|
||||
src/interface/widgets/tabs/tab_visual_database_display.cpp
|
||||
src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp
|
||||
|
|
|
|||
|
|
@ -277,8 +277,6 @@ void GameEventHandler::eventSpectatorLeave(const Event_Leave &event,
|
|||
emit spectatorLeft(eventPlayerId);
|
||||
|
||||
game->getPlayerManager()->removeSpectator(eventPlayerId);
|
||||
|
||||
emitUserEvent();
|
||||
}
|
||||
|
||||
void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event,
|
||||
|
|
@ -465,9 +463,9 @@ void GameEventHandler::eventJoin(const Event_Join &event, int /*eventPlayerId*/,
|
|||
PlayerLogic *newPlayer = game->getPlayerManager()->addPlayer(playerId, playerInfo.user_info());
|
||||
emit logJoinPlayer(newPlayer);
|
||||
emit playerJoined(playerInfo);
|
||||
}
|
||||
|
||||
emitUserEvent();
|
||||
emitUserEvent();
|
||||
}
|
||||
}
|
||||
|
||||
QString GameEventHandler::getLeaveReason(Event_Leave::LeaveReason reason)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
#include "path_safety_warning.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QMessageBox>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
|
||||
class QWidget;
|
||||
|
||||
bool PathSafetyWarning::confirmOutsideProgramDir(QWidget *parent, const QString &path)
|
||||
{
|
||||
if (SettingsCache::instance().getIsPortableBuild()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!PathsSettings::isInsideDir(path, QCoreApplication::applicationDirPath())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QMessageBox box(QMessageBox::Warning, QObject::tr("Path inside the program directory"),
|
||||
QObject::tr("'%1' is inside the Cockatrice program directory.\n\n"
|
||||
"Files stored there can be deleted by an update or by uninstalling "
|
||||
"Cockatrice, so this change was not applied. Choose a location outside "
|
||||
"the program directory.")
|
||||
.arg(path),
|
||||
QMessageBox::Ok, parent);
|
||||
box.setTextFormat(Qt::PlainText);
|
||||
box.exec();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
#ifndef COCKATRICE_PATH_SAFETY_WARNING_H
|
||||
#define COCKATRICE_PATH_SAFETY_WARNING_H
|
||||
|
||||
class QString;
|
||||
class QWidget;
|
||||
|
||||
namespace PathSafetyWarning
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Warns when the user tries to configure a data path inside the program directory.
|
||||
*
|
||||
* Shows a message box explaining that files kept there can be deleted by an
|
||||
* update or by uninstalling Cockatrice. Portable builds are always exempt:
|
||||
* their data lives beside the executables by design.
|
||||
*
|
||||
* @param parent The parent widget
|
||||
* @param path The path the user is trying to configure
|
||||
* @return true when the path is outside the program directory and safe to apply
|
||||
*/
|
||||
bool confirmOutsideProgramDir(QWidget *parent, const QString &path);
|
||||
|
||||
} // namespace PathSafetyWarning
|
||||
|
||||
#endif // COCKATRICE_PATH_SAFETY_WARNING_H
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
/**
|
||||
* @file dlg_settings.cpp
|
||||
* @brief Implementation of the main settings dialog
|
||||
* @file settings_view.cpp
|
||||
* @brief Implementation of the settings content widget
|
||||
* @ingroup Dialogs
|
||||
*/
|
||||
#include "dlg_settings.h"
|
||||
#include "settings_view.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
|
|
@ -19,39 +19,30 @@
|
|||
#include "../settings_page/sound_settings_page.h"
|
||||
#include "../settings_page/storage_settings_page.h"
|
||||
#include "../settings_page/user_interface_settings_page.h"
|
||||
#include "libcockatrice/card/database/card_database.h"
|
||||
#include "libcockatrice/card/database/card_database_loader.h"
|
||||
#include "libcockatrice/card/database/card_database_manager.h"
|
||||
|
||||
#include <QAbstractAnimation>
|
||||
#include <QAbstractItemView>
|
||||
#include <QApplication>
|
||||
#include <QBrush>
|
||||
#include <QByteArray>
|
||||
#include <QCloseEvent>
|
||||
#include <QColor>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QEasingCurve>
|
||||
#include <QEvent>
|
||||
#include <QFlags>
|
||||
#include <QFrame>
|
||||
#include <QGraphicsOpacityEffect>
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QIcon>
|
||||
#include <QKeyEvent>
|
||||
#include <QKeySequence>
|
||||
#include <QLineEdit>
|
||||
#include <QListView>
|
||||
#include <QMessageBox>
|
||||
#include <QMessageLogger>
|
||||
#include <QModelIndex>
|
||||
#include <QObject>
|
||||
#include <QPalette>
|
||||
#include <QPixmap>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QPushButton>
|
||||
#include <QRect>
|
||||
#include <QScreen>
|
||||
#include <QScrollArea>
|
||||
|
|
@ -63,16 +54,13 @@
|
|||
#include <QStackedLayout>
|
||||
#include <QStackedWidget>
|
||||
#include <QStringLiteral>
|
||||
#include <QToolButton>
|
||||
#include <QTreeWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <QtGlobal>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <qnamespace.h>
|
||||
|
||||
class QModelIndex;
|
||||
|
||||
/**
|
||||
* @brief Wraps a widget in a scroll area for long settings pages
|
||||
* @param widget The widget to wrap
|
||||
|
|
@ -102,37 +90,34 @@ static QStringList pageIconResources()
|
|||
QStringLiteral("theme:config/sound"), QStringLiteral("theme:config/shorcuts")};
|
||||
}
|
||||
|
||||
DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent), currentTabIndex(0), searchActive(false)
|
||||
SettingsView::SettingsView(QWidget *parent) : QWidget(parent), currentTabIndex(0), searchActive(false)
|
||||
{
|
||||
auto rec = QGuiApplication::primaryScreen()->availableGeometry();
|
||||
setMinimumSize(qMin(750, rec.width()), qMin(700, rec.height()));
|
||||
|
||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage);
|
||||
|
||||
// Swap the QTranslator before retranslating (was DlgSettings::updateLanguage, removed with the dialog):
|
||||
// without this, picking a new language only stores the setting and every retranslateUi re-runs against the
|
||||
// old translator until restart. Connected before setupUi() so the swap runs before the pages' own handlers.
|
||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &SettingsView::updateLanguage);
|
||||
setupUi();
|
||||
|
||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi);
|
||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &SettingsView::retranslateUi);
|
||||
retranslateUi();
|
||||
|
||||
searchEdit->setFocus();
|
||||
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
void DlgSettings::setupUi()
|
||||
void SettingsView::setupUi()
|
||||
{
|
||||
// Search bar
|
||||
searchEdit = new QLineEdit;
|
||||
searchEdit->setClearButtonEnabled(true);
|
||||
searchEdit->addAction(themePixmap(QStringLiteral("icons/search")), QLineEdit::LeadingPosition);
|
||||
searchEdit->installEventFilter(this);
|
||||
connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged);
|
||||
connect(searchEdit, &QLineEdit::textChanged, this, &SettingsView::onSearchTextChanged);
|
||||
|
||||
auto *searchLayout = new QHBoxLayout;
|
||||
searchLayout->addWidget(searchEdit);
|
||||
|
||||
// Tab bar (built in setupTabBar)
|
||||
setupTabBar();
|
||||
// Navigation tree (built in setupNavTree)
|
||||
setupNavTree();
|
||||
|
||||
// Pages stacked widget
|
||||
pagesWidget = new QStackedWidget;
|
||||
|
|
@ -179,12 +164,14 @@ void DlgSettings::setupUi()
|
|||
searchDelegate = new SettingsSearchDelegate(this);
|
||||
searchResultsView->setModel(searchModel);
|
||||
searchResultsView->setItemDelegate(searchDelegate);
|
||||
connect(searchResultsView, &QListView::clicked, this, &DlgSettings::onSearchResultClicked);
|
||||
connect(searchResultsView, &QListView::clicked, this, &SettingsView::onSearchResultClicked);
|
||||
|
||||
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, [this] {
|
||||
const QStringList icons = pageIconResources();
|
||||
for (int i = 0; i < tabButtons.size() && i < icons.size(); ++i) {
|
||||
tabButtons[i]->setIcon(QPixmap(icons[i]));
|
||||
for (int i = 0; i < pageItems.size() && i < icons.size(); ++i) {
|
||||
if (pageItems[i]) {
|
||||
pageItems[i]->setIcon(0, QPixmap(icons[i]));
|
||||
}
|
||||
}
|
||||
searchDelegate->setPageIcons(icons);
|
||||
searchResultsView->viewport()->update();
|
||||
|
|
@ -201,25 +188,14 @@ void DlgSettings::setupUi()
|
|||
containerLayout->addWidget(searchResultsView);
|
||||
pagesContainer->setLayout(containerLayout);
|
||||
|
||||
// Bottom buttons
|
||||
auto *buttonBox = new QHBoxLayout;
|
||||
buttonBox->addStretch();
|
||||
okButton = new QPushButton;
|
||||
okButton->setDefault(true);
|
||||
connect(okButton, &QPushButton::clicked, this, &DlgSettings::close);
|
||||
buttonBox->addWidget(okButton);
|
||||
// Main layout: search on top, navigation + pages side by side below
|
||||
auto *contentLayout = new QHBoxLayout;
|
||||
contentLayout->addWidget(navTree);
|
||||
contentLayout->addWidget(pagesContainer, 1);
|
||||
|
||||
// Main layout
|
||||
auto *mainLayout = new QVBoxLayout;
|
||||
mainLayout->addLayout(searchLayout);
|
||||
mainLayout->addWidget(tabBarWidget);
|
||||
auto *separator = new QFrame;
|
||||
separator->setFrameShape(QFrame::HLine);
|
||||
separator->setFrameShadow(QFrame::Sunken);
|
||||
mainLayout->addWidget(separator);
|
||||
mainLayout->addWidget(pagesContainer);
|
||||
mainLayout->addSpacing(4);
|
||||
mainLayout->addLayout(buttonBox);
|
||||
mainLayout->addLayout(contentLayout);
|
||||
setLayout(mainLayout);
|
||||
|
||||
// Keyboard shortcuts
|
||||
|
|
@ -228,13 +204,13 @@ void DlgSettings::setupUi()
|
|||
|
||||
auto *nextTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Tab), this);
|
||||
connect(nextTabShortcut, &QShortcut::activated, this, [this] {
|
||||
int next = (currentTabIndex + 1) % tabButtons.size();
|
||||
int next = (currentTabIndex + 1) % pages.size();
|
||||
setActiveTab(next);
|
||||
});
|
||||
|
||||
auto *prevTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Tab), this);
|
||||
connect(prevTabShortcut, &QShortcut::activated, this, [this] {
|
||||
int prev = (currentTabIndex - 1 + tabButtons.size()) % tabButtons.size();
|
||||
int prev = (currentTabIndex - 1 + pages.size()) % pages.size();
|
||||
setActiveTab(prev);
|
||||
});
|
||||
|
||||
|
|
@ -242,35 +218,59 @@ void DlgSettings::setupUi()
|
|||
setActiveTab(0);
|
||||
}
|
||||
|
||||
void DlgSettings::setupTabBar()
|
||||
void SettingsView::setupNavTree()
|
||||
{
|
||||
tabBarWidget = new QWidget;
|
||||
auto *tabLayout = new QHBoxLayout;
|
||||
tabLayout->setContentsMargins(0, 0, 0, 0);
|
||||
tabLayout->setSpacing(2);
|
||||
navTree = new QTreeWidget;
|
||||
navTree->setHeaderHidden(true);
|
||||
navTree->setRootIsDecorated(true);
|
||||
navTree->setIconSize(QSize(24, 24));
|
||||
navTree->setMinimumWidth(230);
|
||||
navTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
navTree->setStyleSheet(
|
||||
"QTreeWidget::item:selected { background: palette(highlight); color: palette(highlighted-text); }");
|
||||
|
||||
// Groups of SettingsPage values, in navigation order
|
||||
const QList<QList<SettingsPage>> groupedPages = {
|
||||
{GeneralPage, AppearancePage, UserInterfacePage},
|
||||
{DeckEditorPage, StoragePage},
|
||||
{MessagesPage, SoundPage},
|
||||
{ShortcutsPage},
|
||||
};
|
||||
|
||||
const QStringList groupNames = translatedGroupNames();
|
||||
const QStringList pageNames = translatedPageNames();
|
||||
const QStringList iconResources = pageIconResources();
|
||||
|
||||
for (int i = 0; i < iconResources.size(); ++i) {
|
||||
auto *tabButton = new QToolButton;
|
||||
tabButton->setCheckable(true);
|
||||
tabButton->setIcon(QPixmap(iconResources[i]));
|
||||
tabButton->setIconSize(QSize(48, 48));
|
||||
tabButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
|
||||
tabButton->setAutoExclusive(true);
|
||||
tabButton->setMinimumHeight(85);
|
||||
tabButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
|
||||
connect(tabButton, &QToolButton::clicked, this, [this, idx = i] { onTabClicked(idx); });
|
||||
|
||||
tabButtons.append(tabButton);
|
||||
tabLayout->addWidget(tabButton);
|
||||
pageItems.resize(NumPages);
|
||||
for (int i = 0; i < groupedPages.size(); ++i) {
|
||||
auto *group = new QTreeWidgetItem(navTree, {groupNames[i]});
|
||||
group->setFlags(Qt::ItemIsEnabled); // header row: expandable, not selectable
|
||||
group->setExpanded(true);
|
||||
groupItems.append(group);
|
||||
for (SettingsPage page : groupedPages[i]) {
|
||||
auto *item = new QTreeWidgetItem(group, {pageNames[page]});
|
||||
item->setIcon(0, QPixmap(iconResources[page]));
|
||||
item->setData(0, Qt::UserRole, static_cast<int>(page));
|
||||
pageItems[page] = item;
|
||||
}
|
||||
}
|
||||
|
||||
tabBarWidget->setLayout(tabLayout);
|
||||
Q_ASSERT(groupItems.size() == groupedPages.size());
|
||||
Q_ASSERT(!pageItems.contains(nullptr));
|
||||
|
||||
connect(navTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *current) {
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
bool ok = false;
|
||||
int index = current->data(0, Qt::UserRole).toInt(&ok);
|
||||
if (ok) {
|
||||
onTabClicked(index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DlgSettings::buildSearchIndex()
|
||||
void SettingsView::buildSearchIndex()
|
||||
{
|
||||
QList<SettingsSearchEntry> allEntries;
|
||||
|
||||
|
|
@ -291,42 +291,34 @@ void DlgSettings::buildSearchIndex()
|
|||
searchModel->setSourceEntries(allEntries);
|
||||
}
|
||||
|
||||
void DlgSettings::onTabClicked(int index)
|
||||
void SettingsView::onTabClicked(int index)
|
||||
{
|
||||
if (searchActive) {
|
||||
switchToTabMode();
|
||||
} else if (index == currentTabIndex) {
|
||||
return;
|
||||
}
|
||||
setActiveTab(index);
|
||||
}
|
||||
|
||||
void DlgSettings::setActiveTab(int index)
|
||||
void SettingsView::setActiveTab(int index)
|
||||
{
|
||||
if (index < 0 || index >= tabButtons.size()) {
|
||||
if (index < 0 || index >= pageItems.size() || !pageItems[index]) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentTabIndex = index;
|
||||
pagesWidget->setCurrentIndex(index);
|
||||
|
||||
for (int i = 0; i < tabButtons.size(); ++i) {
|
||||
tabButtons[i]->setChecked(i == index);
|
||||
}
|
||||
|
||||
// Style active tab with a thick accent border + subtle background tint
|
||||
for (int i = 0; i < tabButtons.size(); ++i) {
|
||||
if (i == index) {
|
||||
tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 3px solid palette(highlight); "
|
||||
"border-top-left-radius: 4px; border-top-right-radius: 4px; "
|
||||
"background: palette(window); padding-bottom: 1px; }");
|
||||
} else {
|
||||
tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 1px solid transparent; "
|
||||
"border-top-left-radius: 4px; border-top-right-radius: 4px; "
|
||||
"background: transparent; }");
|
||||
}
|
||||
// Selecting the item also expands a collapsed group; selecting the already
|
||||
// current item emits nothing, so the currentItemChanged handler cannot loop.
|
||||
if (QTreeWidgetItem *parent = pageItems[index]->parent()) {
|
||||
parent->setExpanded(true);
|
||||
}
|
||||
navTree->setCurrentItem(pageItems[index]);
|
||||
}
|
||||
|
||||
void DlgSettings::flashWidget(QWidget *widget)
|
||||
void SettingsView::flashWidget(QWidget *widget)
|
||||
{
|
||||
auto *overlay = new QWidget(widget);
|
||||
overlay->setGeometry(widget->rect());
|
||||
|
|
@ -366,7 +358,7 @@ void DlgSettings::flashWidget(QWidget *widget)
|
|||
group->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
}
|
||||
|
||||
void DlgSettings::onSearchTextChanged(const QString &text)
|
||||
void SettingsView::onSearchTextChanged(const QString &text)
|
||||
{
|
||||
searchModel->setFilterString(text);
|
||||
|
||||
|
|
@ -382,10 +374,10 @@ void DlgSettings::onSearchTextChanged(const QString &text)
|
|||
}
|
||||
}
|
||||
|
||||
void DlgSettings::switchToSearchMode()
|
||||
void SettingsView::switchToSearchMode()
|
||||
{
|
||||
searchActive = true;
|
||||
tabBarWidget->setVisible(false);
|
||||
navTree->setVisible(false);
|
||||
pagesWidget->setVisible(false);
|
||||
searchResultsView->setVisible(true);
|
||||
if (searchModel->rowCount(QModelIndex()) > 0) {
|
||||
|
|
@ -393,10 +385,10 @@ void DlgSettings::switchToSearchMode()
|
|||
}
|
||||
}
|
||||
|
||||
void DlgSettings::switchToTabMode()
|
||||
void SettingsView::switchToTabMode()
|
||||
{
|
||||
searchActive = false;
|
||||
tabBarWidget->setVisible(true);
|
||||
navTree->setVisible(true);
|
||||
pagesWidget->setVisible(true);
|
||||
searchResultsView->setVisible(false);
|
||||
searchEdit->blockSignals(true);
|
||||
|
|
@ -405,12 +397,12 @@ void DlgSettings::switchToTabMode()
|
|||
setActiveTab(currentTabIndex);
|
||||
}
|
||||
|
||||
void DlgSettings::onSearchResultClicked(const QModelIndex &index)
|
||||
void SettingsView::onSearchResultClicked(const QModelIndex &index)
|
||||
{
|
||||
navigateToSearchResult(index);
|
||||
}
|
||||
|
||||
void DlgSettings::navigateToSearchResult(const QModelIndex &index)
|
||||
void SettingsView::navigateToSearchResult(const QModelIndex &index)
|
||||
{
|
||||
SettingsSearchEntry entry = searchModel->entryForIndex(index);
|
||||
if (entry.pageIndex < 0 || entry.pageIndex >= pages.size()) {
|
||||
|
|
@ -436,25 +428,19 @@ void DlgSettings::navigateToSearchResult(const QModelIndex &index)
|
|||
}
|
||||
}
|
||||
|
||||
void DlgSettings::setTab(int index)
|
||||
void SettingsView::setTab(int index)
|
||||
{
|
||||
if (index >= 0 && index < tabButtons.size()) {
|
||||
if (index >= 0 && index < pageItems.size()) {
|
||||
setActiveTab(index);
|
||||
}
|
||||
}
|
||||
|
||||
AbstractSettingsPage *DlgSettings::page(SettingsPage which) const
|
||||
AbstractSettingsPage *SettingsView::page(SettingsPage which) const
|
||||
{
|
||||
return pages.value(static_cast<int>(which));
|
||||
}
|
||||
|
||||
void DlgSettings::updateLanguage()
|
||||
{
|
||||
qApp->removeTranslator(translator); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
|
||||
installNewTranslator();
|
||||
}
|
||||
|
||||
bool DlgSettings::eventFilter(QObject *watched, QEvent *event)
|
||||
bool SettingsView::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (watched == searchEdit && event->type() == QEvent::KeyPress) {
|
||||
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
||||
|
|
@ -494,108 +480,56 @@ bool DlgSettings::eventFilter(QObject *watched, QEvent *event)
|
|||
}
|
||||
}
|
||||
}
|
||||
return QDialog::eventFilter(watched, event);
|
||||
return QWidget::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
void DlgSettings::closeEvent(QCloseEvent *event)
|
||||
void SettingsView::updateLanguage()
|
||||
{
|
||||
bool showLoadError = true;
|
||||
QString loadErrorMessage = tr("Unknown Error loading card database");
|
||||
LoadStatus loadStatus = CardDatabaseManager::getInstance()->getLoadStatus();
|
||||
qCInfo(DlgSettingsLog) << "Card Database load status: " << loadStatus;
|
||||
switch (loadStatus) {
|
||||
case Ok:
|
||||
showLoadError = false;
|
||||
break;
|
||||
case Invalid:
|
||||
loadErrorMessage = tr("Your card database is invalid.\n\n"
|
||||
"Cockatrice may not function correctly with an invalid database\n\n"
|
||||
"You may need to rerun Oracle to update your card database.\n\n"
|
||||
"Would you like to change your database location setting?");
|
||||
break;
|
||||
case VersionTooOld:
|
||||
loadErrorMessage = tr("Your card database version is too old.\n\n"
|
||||
"This can cause problems loading card information or images\n\n"
|
||||
"Usually this can be fixed by rerunning Oracle to to update your card database.\n\n"
|
||||
"Would you like to change your database location setting?");
|
||||
break;
|
||||
case NotLoaded:
|
||||
loadErrorMessage = tr("Your card database did not finish loading\n\n"
|
||||
"Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues with your "
|
||||
"cards.xml attached\n\n"
|
||||
"Would you like to change your database location setting?");
|
||||
break;
|
||||
case FileError:
|
||||
loadErrorMessage = tr("File Error loading your card database.\n\n"
|
||||
"Would you like to change your database location setting?");
|
||||
break;
|
||||
case NoCards:
|
||||
loadErrorMessage = tr("Your card database was loaded but contains no cards.\n\n"
|
||||
"Would you like to change your database location setting?");
|
||||
break;
|
||||
default:
|
||||
loadErrorMessage = tr("Unknown card database load status\n\n"
|
||||
"Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues\n\n"
|
||||
"Would you like to change your database location setting?");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (showLoadError) {
|
||||
if (QMessageBox::critical(this, tr("Error"), loadErrorMessage, QMessageBox::Yes | QMessageBox::No) ==
|
||||
QMessageBox::Yes) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!QDir(SettingsCache::instance().paths().getDeckPath()).exists() ||
|
||||
SettingsCache::instance().paths().getDeckPath().isEmpty()) {
|
||||
if (QMessageBox::critical(
|
||||
this, tr("Error"),
|
||||
tr("The path to your deck directory is invalid. Would you like to go back and set the correct path?"),
|
||||
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!QDir(SettingsCache::instance().paths().getPicsPath()).exists() ||
|
||||
SettingsCache::instance().paths().getPicsPath().isEmpty()) {
|
||||
if (QMessageBox::critical(this, tr("Error"),
|
||||
tr("The path to your card pictures directory is invalid. Would you like to go back "
|
||||
"and set the correct path?"),
|
||||
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
}
|
||||
event->accept();
|
||||
qApp->removeTranslator(translator); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
|
||||
installNewTranslator();
|
||||
}
|
||||
|
||||
void DlgSettings::retranslateUi()
|
||||
void SettingsView::retranslateUi()
|
||||
{
|
||||
setWindowTitle(tr("Settings"));
|
||||
retranslateTabNames();
|
||||
retranslateNavNames();
|
||||
|
||||
searchEdit->setPlaceholderText(tr("Search settings..."));
|
||||
okButton->setText(tr("OK"));
|
||||
// NOTE: the "DlgSettings" context is deliberate — these strings lived in DlgSettings
|
||||
// before the view was extracted, and keeping the context preserves existing translations.
|
||||
searchEdit->setPlaceholderText(QCoreApplication::translate("DlgSettings", "Search settings..."));
|
||||
|
||||
// Rebuild search index for translated text
|
||||
buildSearchIndex();
|
||||
}
|
||||
|
||||
QStringList DlgSettings::translatedPageNames()
|
||||
QStringList SettingsView::translatedPageNames()
|
||||
{
|
||||
return {tr("General"), tr("Appearance"), tr("User Interface"), tr("Card Sources"),
|
||||
tr("Storage"), tr("Chat"), tr("Sound"), tr("Shortcuts")};
|
||||
// NOTE: same "DlgSettings" context as above (translation continuity, see retranslateUi).
|
||||
return {QCoreApplication::translate("DlgSettings", "General"),
|
||||
QCoreApplication::translate("DlgSettings", "Appearance"),
|
||||
QCoreApplication::translate("DlgSettings", "User Interface"),
|
||||
QCoreApplication::translate("DlgSettings", "Card Sources"),
|
||||
QCoreApplication::translate("DlgSettings", "Storage"),
|
||||
QCoreApplication::translate("DlgSettings", "Chat"),
|
||||
QCoreApplication::translate("DlgSettings", "Sound"),
|
||||
QCoreApplication::translate("DlgSettings", "Shortcuts")};
|
||||
}
|
||||
|
||||
void DlgSettings::retranslateTabNames()
|
||||
void SettingsView::retranslateNavNames()
|
||||
{
|
||||
const QStringList tabLabels = translatedPageNames();
|
||||
const QStringList groupNames = translatedGroupNames();
|
||||
for (int i = 0; i < groupItems.size() && i < groupNames.size(); ++i) {
|
||||
groupItems[i]->setText(0, groupNames[i]);
|
||||
}
|
||||
|
||||
for (int i = 0; i < tabButtons.size() && i < tabLabels.size(); ++i) {
|
||||
tabButtons[i]->setText(tabLabels[i]);
|
||||
const QStringList pageNames = translatedPageNames();
|
||||
for (int i = 0; i < pageItems.size() && i < pageNames.size(); ++i) {
|
||||
if (pageItems[i]) {
|
||||
pageItems[i]->setText(0, pageNames[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QStringList SettingsView::translatedGroupNames()
|
||||
{
|
||||
return {tr("Application"), tr("Cards & Data"), tr("Chat & Sound"), tr("Advanced")};
|
||||
}
|
||||
|
|
@ -1,26 +1,24 @@
|
|||
/**
|
||||
* @file dlg_settings.h
|
||||
* @brief Main settings dialog for the Cockatrice client
|
||||
* @file settings_view.h
|
||||
* @brief Settings content widget hosted by the settings dialog (and, later, the settings tab)
|
||||
* @ingroup Dialogs
|
||||
*/
|
||||
#ifndef DLG_SETTINGS_H
|
||||
#define DLG_SETTINGS_H
|
||||
#ifndef SETTINGS_VIEW_H
|
||||
#define SETTINGS_VIEW_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QWidget>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class QPushButton;
|
||||
class QModelIndex;
|
||||
class QWidget;
|
||||
|
||||
inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings");
|
||||
class QObject;
|
||||
class QEvent;
|
||||
|
||||
class QStackedWidget;
|
||||
class QToolButton;
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
class QListView;
|
||||
class QLineEdit;
|
||||
class AbstractSettingsPage;
|
||||
|
|
@ -28,18 +26,17 @@ class SettingsSearchModel;
|
|||
class SettingsSearchDelegate;
|
||||
|
||||
/**
|
||||
* @brief Main application settings dialog with tabbed navigation and search
|
||||
* @brief Settings pages with grouped left navigation and search, without any dialog chrome
|
||||
*
|
||||
* Provides a modern settings interface organized into tabbed pages. Users can
|
||||
* either navigate by clicking tabs or search for specific settings using the
|
||||
* built-in search bar. Search results are filtered and ranked by relevance.
|
||||
* Owns the search bar, the navigation tree, the stacked settings pages and the search
|
||||
* results view. Dialog/tab hosts only add their own frame (buttons, titles).
|
||||
*/
|
||||
class DlgSettings : public QDialog
|
||||
class SettingsView : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
/**
|
||||
* @brief Page order in the tab bar, matching the order pages are added in setupUi()
|
||||
* @brief Page order in the navigation, matching the order pages are added in setupUi()
|
||||
*
|
||||
* Use these values instead of raw indices so reordering pages never silently
|
||||
* breaks external callers like tab_room.cpp.
|
||||
|
|
@ -57,7 +54,7 @@ public:
|
|||
NumPages
|
||||
};
|
||||
|
||||
explicit DlgSettings(QWidget *parent = nullptr);
|
||||
explicit SettingsView(QWidget *parent = nullptr);
|
||||
void setTab(int index);
|
||||
AbstractSettingsPage *page(SettingsPage which) const;
|
||||
|
||||
|
|
@ -69,13 +66,13 @@ private slots:
|
|||
|
||||
private:
|
||||
// UI elements
|
||||
QLineEdit *searchEdit; ///< Search bar for filtering settings
|
||||
QWidget *tabBarWidget; ///< Container widget for the tab buttons
|
||||
QList<QToolButton *> tabButtons; ///< Navigation tab buttons
|
||||
QStackedWidget *pagesWidget; ///< Stacked widget containing settings pages
|
||||
QListView *searchResultsView; ///< Search results list view
|
||||
QWidget *pagesContainer; ///< Container stacking pages and search results
|
||||
QPushButton *okButton; ///< Button to close the dialog
|
||||
QLineEdit *searchEdit; ///< Search bar for filtering settings
|
||||
QTreeWidget *navTree; ///< Grouped navigation tree for the pages
|
||||
QList<QTreeWidgetItem *> groupItems; ///< Group header rows, indexed by group
|
||||
QList<QTreeWidgetItem *> pageItems; ///< Page rows, indexed by SettingsPage
|
||||
QStackedWidget *pagesWidget; ///< Stacked widget containing settings pages
|
||||
QListView *searchResultsView; ///< Search results list view
|
||||
QWidget *pagesContainer; ///< Container stacking pages and search results
|
||||
|
||||
// Data
|
||||
QList<AbstractSettingsPage *> pages; ///< All settings page instances
|
||||
|
|
@ -85,7 +82,7 @@ private:
|
|||
bool searchActive; ///< Whether search mode is active
|
||||
|
||||
void setupUi();
|
||||
void setupTabBar();
|
||||
void setupNavTree();
|
||||
void buildSearchIndex();
|
||||
void switchToTabMode();
|
||||
void switchToSearchMode();
|
||||
|
|
@ -93,13 +90,13 @@ private:
|
|||
void setActiveTab(int index);
|
||||
static void flashWidget(QWidget *widget);
|
||||
static QStringList translatedPageNames();
|
||||
static QStringList translatedGroupNames();
|
||||
|
||||
void retranslateUi();
|
||||
void retranslateTabNames();
|
||||
void retranslateNavNames();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
};
|
||||
|
||||
#endif // DLG_SETTINGS_H
|
||||
#endif // SETTINGS_VIEW_H
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "../dialogs/path_safety_warning.h"
|
||||
#include "update/card_spoiler/spoiler_background_updater.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
|
|
@ -363,6 +364,9 @@ void DeckEditorSettingsPage::spoilerPathButtonClicked()
|
|||
if (lsPath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!PathSafetyWarning::confirmOutsideProgramDir(this, lsPath + "/spoiler.xml")) {
|
||||
return;
|
||||
}
|
||||
|
||||
mpSpoilerSavePathLineEdit->setText(lsPath + "/spoiler.xml");
|
||||
SettingsCache::instance().paths().setSpoilerDatabasePath(lsPath + "/spoiler.xml");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "general_settings_page.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../dialogs/path_safety_warning.h"
|
||||
#include "../interface/card_picture_loader/card_picture_loader.h"
|
||||
#include "../main.h"
|
||||
#include "../server/user/user_info_connection.h"
|
||||
|
|
@ -378,6 +379,9 @@ void GeneralSettingsPage::deckPathButtonClicked()
|
|||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
deckPathEdit->setText(path);
|
||||
SettingsCache::instance().paths().setDeckPath(path);
|
||||
|
|
@ -389,6 +393,9 @@ void GeneralSettingsPage::filtersPathButtonClicked()
|
|||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
filtersPathEdit->setText(path);
|
||||
SettingsCache::instance().paths().setFiltersPath(path);
|
||||
|
|
@ -400,6 +407,9 @@ void GeneralSettingsPage::replaysPathButtonClicked()
|
|||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
replaysPathEdit->setText(path);
|
||||
SettingsCache::instance().paths().setReplaysPath(path);
|
||||
|
|
@ -411,6 +421,9 @@ void GeneralSettingsPage::picsPathButtonClicked()
|
|||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
picsPathEdit->setText(path);
|
||||
SettingsCache::instance().paths().setPicsPath(path);
|
||||
|
|
@ -422,6 +435,9 @@ void GeneralSettingsPage::cardDatabasePathButtonClicked()
|
|||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
cardDatabasePathEdit->setText(path);
|
||||
SettingsCache::instance().paths().setCardDatabasePath(path);
|
||||
|
|
@ -433,6 +449,9 @@ void GeneralSettingsPage::customCardDatabaseButtonClicked()
|
|||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
customCardDatabasePathEdit->setText(path);
|
||||
SettingsCache::instance().paths().setCustomCardDatabasePath(path);
|
||||
|
|
@ -444,6 +463,9 @@ void GeneralSettingsPage::tokenDatabasePathButtonClicked()
|
|||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
tokenDatabasePathEdit->setText(path);
|
||||
SettingsCache::instance().paths().setTokenDatabasePath(path);
|
||||
|
|
|
|||
|
|
@ -1060,6 +1060,12 @@ void TabGame::setCardMenu(CardMenu *menu)
|
|||
return;
|
||||
}
|
||||
|
||||
if (aCardMenu->menu() && aCardMenu->menu()->isVisible()) {
|
||||
aCardMenu->menu()->hide();
|
||||
if (gameView && gameView->viewport()) {
|
||||
gameView->viewport()->update();
|
||||
}
|
||||
}
|
||||
if (menu) {
|
||||
aCardMenu->setMenu(menu);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "tab_report.h"
|
||||
|
||||
#include "../server/user/user_context_menu.h"
|
||||
#include "../utility/report_utils.h"
|
||||
#include "abstract_client.h"
|
||||
#include "libcockatrice/protocol/pb/response.pb.h"
|
||||
|
|
@ -33,11 +34,13 @@
|
|||
#include <QVariant>
|
||||
#include <QWidget>
|
||||
#include <QtGlobal>
|
||||
#include <libcockatrice/network/server/remote/user_level.h>
|
||||
#include <libcockatrice/protocol/pb/command_replay_download_by_game_id.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_add_comment.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_assign.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_details.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_list.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_reopen.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_resolve.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_stats.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_user_info.pb.h>
|
||||
|
|
@ -120,6 +123,10 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T
|
|||
table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch);
|
||||
table->horizontalHeader()->setSectionResizeMode(COL_REPORTER, QHeaderView::ResizeToContents);
|
||||
connect(table, &QTableWidget::itemSelectionChanged, this, &TabReport::onSelectionChanged);
|
||||
table->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(table, &QTableWidget::customContextMenuRequested, this, &TabReport::showTableContextMenu);
|
||||
userContextMenu = new UserContextMenu(tabSupervisor, this);
|
||||
connect(userContextMenu, &UserContextMenu::openMessageDialog, this, &TabReport::openMessageDialog);
|
||||
|
||||
descGroup = new QGroupBox;
|
||||
descriptionEdit = new QTextEdit;
|
||||
|
|
@ -231,14 +238,14 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T
|
|||
|
||||
assignButton = new QPushButton;
|
||||
resolveButton = new QPushButton;
|
||||
resolveWithNoteButton = new QPushButton;
|
||||
dismissButton = new QPushButton;
|
||||
reopenButton = new QPushButton;
|
||||
viewReplayButton = new QPushButton;
|
||||
joinGameButton = new QPushButton;
|
||||
connect(assignButton, &QPushButton::clicked, this, &TabReport::assignReport);
|
||||
connect(resolveButton, &QPushButton::clicked, this, [this]() { resolveReport(false, false); });
|
||||
connect(resolveWithNoteButton, &QPushButton::clicked, this, [this]() { resolveReport(false, true); });
|
||||
connect(dismissButton, &QPushButton::clicked, this, [this]() { resolveReport(true, true); });
|
||||
connect(resolveButton, &QPushButton::clicked, this, [this]() { resolveReport(false); });
|
||||
connect(dismissButton, &QPushButton::clicked, this, [this]() { resolveReport(true); });
|
||||
connect(reopenButton, &QPushButton::clicked, this, &TabReport::reopenReport);
|
||||
connect(viewReplayButton, &QPushButton::clicked, this, &TabReport::viewReplay);
|
||||
connect(joinGameButton, &QPushButton::clicked, this, &TabReport::joinGame);
|
||||
|
||||
|
|
@ -247,8 +254,8 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T
|
|||
auto *actionBar = new QHBoxLayout;
|
||||
actionBar->addWidget(assignButton);
|
||||
actionBar->addWidget(resolveButton);
|
||||
actionBar->addWidget(resolveWithNoteButton);
|
||||
actionBar->addWidget(dismissButton);
|
||||
actionBar->addWidget(reopenButton);
|
||||
actionBar->addSpacing(20);
|
||||
actionBar->addWidget(viewReplayButton);
|
||||
actionBar->addWidget(joinGameButton);
|
||||
|
|
@ -272,6 +279,7 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T
|
|||
|
||||
void TabReport::retranslateUi()
|
||||
{
|
||||
userContextMenu->retranslateUi();
|
||||
searchEdit->setPlaceholderText(tr("Search by username, category..."));
|
||||
statusFilter->clear();
|
||||
statusFilter->addItem(tr("All Statuses"), "");
|
||||
|
|
@ -312,9 +320,10 @@ void TabReport::retranslateUi()
|
|||
statsGroup->setTitle(tr("Report Statistics"));
|
||||
|
||||
assignButton->setText(tr("Assign to Me"));
|
||||
resolveButton->setText(tr("Resolve"));
|
||||
resolveWithNoteButton->setText(tr("Resolve with note..."));
|
||||
resolveButton->setText(tr("Resolve..."));
|
||||
dismissButton->setText(tr("Dismiss..."));
|
||||
reopenButton->setText(tr("Reopen..."));
|
||||
reopenButton->setToolTip(tr("Available for resolved or dismissed reports."));
|
||||
viewReplayButton->setText(tr("View Replay"));
|
||||
joinGameButton->setText(tr("Join Game"));
|
||||
}
|
||||
|
|
@ -346,6 +355,7 @@ void TabReport::reportListResponse(const Response &response)
|
|||
refreshButton->setEnabled(true);
|
||||
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
pendingStatusMessage.clear();
|
||||
statusLabel->setText(tr("Failed to load reports."));
|
||||
return;
|
||||
}
|
||||
|
|
@ -359,6 +369,11 @@ void TabReport::reportListResponse(const Response &response)
|
|||
applyFilters();
|
||||
updateStats();
|
||||
|
||||
if (!pendingStatusMessage.isEmpty()) {
|
||||
statusLabel->setText(pendingStatusMessage);
|
||||
pendingStatusMessage.clear();
|
||||
}
|
||||
|
||||
if (selectedReportIdBeforeRefresh >= 0) {
|
||||
bool found = false;
|
||||
for (int row = 0; row < table->rowCount(); ++row) {
|
||||
|
|
@ -494,6 +509,36 @@ void TabReport::onSelectionChanged()
|
|||
updateActionStates();
|
||||
}
|
||||
|
||||
void TabReport::showTableContextMenu(const QPoint &pos)
|
||||
{
|
||||
QTableWidgetItem *clickedItem = table->itemAt(pos);
|
||||
if (!clickedItem || clickedItem->row() < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int row = clickedItem->row();
|
||||
const QTableWidgetItem *reportedItem = table->item(row, COL_REPORTED);
|
||||
const QTableWidgetItem *idItem = table->item(row, COL_ID);
|
||||
if (!reportedItem || !idItem || reportedItem->text().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userLevel = ServerInfo_User::IsUser;
|
||||
const int reportId = idItem->data(Qt::UserRole).toInt();
|
||||
for (const ServerInfo_Report &r : filteredReports) {
|
||||
if (r.report_id() == reportId) {
|
||||
if (r.reported_user_level() != 0) {
|
||||
userLevel = r.reported_user_level();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
table->selectRow(row);
|
||||
userContextMenu->showContextMenu(table->viewport()->mapToGlobal(pos), reportedItem->text(),
|
||||
UserLevelFlags(userLevel));
|
||||
}
|
||||
|
||||
void TabReport::loadReportDetails(int reportId)
|
||||
{
|
||||
detailsRequestedReportId = reportId;
|
||||
|
|
@ -577,8 +622,8 @@ void TabReport::updateActionStates()
|
|||
const QString status = QString::fromStdString(report.status());
|
||||
assignButton->setEnabled(status == "open");
|
||||
resolveButton->setEnabled(status == "open" || status == "assigned");
|
||||
resolveWithNoteButton->setEnabled(status == "open" || status == "assigned");
|
||||
dismissButton->setEnabled(status == "open" || status == "assigned");
|
||||
reopenButton->setEnabled(status == "resolved" || status == "dismissed");
|
||||
|
||||
const bool hasGameId = report.game_id() > 0;
|
||||
const bool hasReplay = hasGameId && report.has_replay_id() && report.replay_id() > 0;
|
||||
|
|
@ -590,8 +635,8 @@ void TabReport::setActionsEnabled(bool enabled)
|
|||
{
|
||||
assignButton->setEnabled(enabled);
|
||||
resolveButton->setEnabled(enabled);
|
||||
resolveWithNoteButton->setEnabled(enabled);
|
||||
dismissButton->setEnabled(enabled);
|
||||
reopenButton->setEnabled(enabled);
|
||||
viewReplayButton->setEnabled(false);
|
||||
joinGameButton->setEnabled(false);
|
||||
commentButton->setEnabled(enabled);
|
||||
|
|
@ -645,7 +690,7 @@ void TabReport::assignReport()
|
|||
void TabReport::assignResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() == Response::RespOk) {
|
||||
statusLabel->setText(tr("Assigned."));
|
||||
pendingStatusMessage = tr("Assigned.");
|
||||
refreshList();
|
||||
} else {
|
||||
statusLabel->setText(tr("Assignment failed."));
|
||||
|
|
@ -653,22 +698,19 @@ void TabReport::assignResponse(const Response &response)
|
|||
}
|
||||
}
|
||||
|
||||
void TabReport::resolveReport(bool dismissed, bool promptNote)
|
||||
void TabReport::resolveReport(bool dismissed)
|
||||
{
|
||||
const int reportId = selectedReportId();
|
||||
if (reportId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString note;
|
||||
if (promptNote) {
|
||||
bool ok;
|
||||
note = QInputDialog::getText(this, dismissed ? tr("Dismiss Report") : tr("Resolve Report"),
|
||||
dismissed ? tr("Optional note:") : tr("Resolution note (optional):"),
|
||||
QLineEdit::Normal, QString(), &ok);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
bool ok;
|
||||
QString note = QInputDialog::getText(this, dismissed ? tr("Dismiss Report") : tr("Resolve Report"),
|
||||
dismissed ? tr("Optional note:") : tr("Resolution note (optional):"),
|
||||
QLineEdit::Normal, QString(), &ok);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActionsEnabled(false);
|
||||
|
|
@ -689,7 +731,46 @@ void TabReport::resolveReport(bool dismissed, bool promptNote)
|
|||
void TabReport::resolveResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() == Response::RespOk) {
|
||||
statusLabel->setText(tr("Done."));
|
||||
pendingStatusMessage = tr("Done.");
|
||||
refreshList();
|
||||
} else {
|
||||
statusLabel->setText(tr("Action failed."));
|
||||
updateActionStates();
|
||||
}
|
||||
}
|
||||
|
||||
void TabReport::reopenReport()
|
||||
{
|
||||
const int reportId = selectedReportId();
|
||||
if (reportId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
QString note = QInputDialog::getText(this, tr("Reopen Report"), tr("Reopening note (optional):"), QLineEdit::Normal,
|
||||
QString(), &ok);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActionsEnabled(false);
|
||||
statusLabel->setText(tr("Reopening..."));
|
||||
|
||||
Command_ReportReopen cmd;
|
||||
cmd.set_report_id(reportId);
|
||||
if (!note.isEmpty()) {
|
||||
cmd.set_reopen_note(note.toStdString());
|
||||
}
|
||||
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabReport::reopenResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabReport::reopenResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() == Response::RespOk) {
|
||||
pendingStatusMessage = tr("Reopened.");
|
||||
refreshList();
|
||||
} else {
|
||||
statusLabel->setText(tr("Action failed."));
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "tab.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QPoint>
|
||||
#include <QString>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_report.pb.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
|
@ -22,6 +23,7 @@ class QTimer;
|
|||
class GameReplay;
|
||||
class Response;
|
||||
class TabSupervisor;
|
||||
class UserContextMenu;
|
||||
|
||||
class TabReport : public Tab
|
||||
{
|
||||
|
|
@ -37,15 +39,17 @@ public:
|
|||
signals:
|
||||
void openReplay(GameReplay *replay);
|
||||
void requestJoinGame(int gameId, int roomId);
|
||||
void openMessageDialog(const QString &userName, bool focus);
|
||||
|
||||
private slots:
|
||||
void refreshList();
|
||||
void reportListResponse(const Response &response);
|
||||
void assignReport();
|
||||
void assignResponse(const Response &response);
|
||||
void resolveReport(bool dismissed, bool promptNote);
|
||||
void resolveReport(bool dismissed);
|
||||
void resolveResponse(const Response &response);
|
||||
void onSelectionChanged();
|
||||
void showTableContextMenu(const QPoint &pos);
|
||||
void viewReplay();
|
||||
void viewReplayResponse(const Response &response);
|
||||
void joinGame();
|
||||
|
|
@ -56,6 +60,8 @@ private slots:
|
|||
void reportDetailsResponse(const Response &response);
|
||||
void requestStats();
|
||||
void statsResponse(const Response &response);
|
||||
void reopenReport();
|
||||
void reopenResponse(const Response &response);
|
||||
|
||||
private:
|
||||
int selectedReportId() const;
|
||||
|
|
@ -85,12 +91,13 @@ private:
|
|||
QPushButton *commentButton;
|
||||
QPushButton *assignButton;
|
||||
QPushButton *resolveButton;
|
||||
QPushButton *resolveWithNoteButton;
|
||||
QPushButton *dismissButton;
|
||||
QPushButton *reopenButton;
|
||||
QPushButton *viewReplayButton;
|
||||
QPushButton *joinGameButton;
|
||||
QLabel *statusLabel;
|
||||
QTimer *refreshTimer;
|
||||
UserContextMenu *userContextMenu;
|
||||
|
||||
QGroupBox *userContextGroup;
|
||||
QLabel *userContextName;
|
||||
|
|
@ -108,6 +115,7 @@ private:
|
|||
QLabel *userContextNotesLabel;
|
||||
QLabel *userContextRecentReportsLabel;
|
||||
QString lastRequestedUser;
|
||||
QString pendingStatusMessage;
|
||||
QGroupBox *statsGroup;
|
||||
QLabel *statsTotalLabel;
|
||||
QLabel *statsTrendLabel;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "../interface/widgets/dialogs/dlg_settings.h"
|
||||
#include "../interface/widgets/dialogs/settings_view.h"
|
||||
#include "../interface/widgets/server/chat_view/chat_view.h"
|
||||
#include "../interface/widgets/server/game_link.h"
|
||||
#include "../interface/widgets/server/game_selector.h"
|
||||
|
|
@ -272,9 +272,7 @@ void TabRoom::actClearChat()
|
|||
|
||||
void TabRoom::actOpenChatSettings()
|
||||
{
|
||||
DlgSettings settings(this);
|
||||
settings.setTab(DlgSettings::MessagesPage);
|
||||
settings.exec();
|
||||
tabSupervisor->openTabSettings(SettingsView::MessagesPage);
|
||||
}
|
||||
|
||||
void TabRoom::actCompleterChanged()
|
||||
|
|
|
|||
129
cockatrice/src/interface/widgets/tabs/tab_settings.cpp
Normal file
129
cockatrice/src/interface/widgets/tabs/tab_settings.cpp
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/**
|
||||
* @file tab_settings.cpp
|
||||
* @ingroup Tabs
|
||||
*/
|
||||
#include "tab_settings.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../dialogs/settings_view.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMessageBox>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_loader.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(TabSettingsLog, "tab_settings");
|
||||
|
||||
TabSettings::TabSettings(TabSupervisor *_tabSupervisor, int pageIndex) : Tab(_tabSupervisor)
|
||||
{
|
||||
settingsView = new SettingsView(this);
|
||||
setCentralWidget(settingsView);
|
||||
if (pageIndex >= 0) {
|
||||
settingsView->setTab(pageIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void TabSettings::setTab(int pageIndex)
|
||||
{
|
||||
settingsView->setTab(pageIndex);
|
||||
}
|
||||
|
||||
void TabSettings::retranslateUi()
|
||||
{
|
||||
// Nothing to do: SettingsView retranslates itself via langChanged and the
|
||||
// TabSupervisor refreshes the tab title from getTabText().
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Validates paths and card database before the tab closes
|
||||
*
|
||||
* Moved verbatim from DlgSettings::closeEvent. The "DlgSettings" translation
|
||||
* context below is deliberate and preserves the existing translations.
|
||||
*
|
||||
* @return False when the user wants to go back and fix a setting, true otherwise
|
||||
*/
|
||||
bool TabSettings::closeRequest()
|
||||
{
|
||||
// NOTE: "DlgSettings" contexts are intentional (translation continuity, see SettingsView).
|
||||
bool showLoadError = true;
|
||||
QString loadErrorMessage = QCoreApplication::translate("DlgSettings", "Unknown Error loading card database");
|
||||
LoadStatus loadStatus = CardDatabaseManager::getInstance()->getLoadStatus();
|
||||
qCInfo(TabSettingsLog) << "Card Database load status: " << loadStatus;
|
||||
switch (loadStatus) {
|
||||
case Ok:
|
||||
showLoadError = false;
|
||||
break;
|
||||
case Invalid:
|
||||
loadErrorMessage = QCoreApplication::translate(
|
||||
"DlgSettings", "Your card database is invalid.\n\n"
|
||||
"Cockatrice may not function correctly with an invalid database\n\n"
|
||||
"You may need to rerun Oracle to update your card database.\n\n"
|
||||
"Would you like to change your database location setting?");
|
||||
break;
|
||||
case VersionTooOld:
|
||||
loadErrorMessage = QCoreApplication::translate(
|
||||
"DlgSettings", "Your card database version is too old.\n\n"
|
||||
"This can cause problems loading card information or images\n\n"
|
||||
"Usually this can be fixed by rerunning Oracle to to update your card database.\n\n"
|
||||
"Would you like to change your database location setting?");
|
||||
break;
|
||||
case NotLoaded:
|
||||
loadErrorMessage = QCoreApplication::translate(
|
||||
"DlgSettings", "Your card database did not finish loading\n\n"
|
||||
"Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues with your "
|
||||
"cards.xml attached\n\n"
|
||||
"Would you like to change your database location setting?");
|
||||
break;
|
||||
case FileError:
|
||||
loadErrorMessage =
|
||||
QCoreApplication::translate("DlgSettings", "File Error loading your card database.\n\n"
|
||||
"Would you like to change your database location setting?");
|
||||
break;
|
||||
case NoCards:
|
||||
loadErrorMessage =
|
||||
QCoreApplication::translate("DlgSettings", "Your card database was loaded but contains no cards.\n\n"
|
||||
"Would you like to change your database location setting?");
|
||||
break;
|
||||
default:
|
||||
loadErrorMessage = QCoreApplication::translate(
|
||||
"DlgSettings", "Unknown card database load status\n\n"
|
||||
"Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues\n\n"
|
||||
"Would you like to change your database location setting?");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (showLoadError) {
|
||||
if (QMessageBox::critical(this, QCoreApplication::translate("DlgSettings", "Error"), loadErrorMessage,
|
||||
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!QDir(SettingsCache::instance().paths().getDeckPath()).exists() ||
|
||||
SettingsCache::instance().paths().getDeckPath().isEmpty()) {
|
||||
if (QMessageBox::critical(this, QCoreApplication::translate("DlgSettings", "Error"),
|
||||
QCoreApplication::translate(
|
||||
"DlgSettings", "The path to your deck directory is invalid. Would you like to "
|
||||
"go back and set the correct path?"),
|
||||
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!QDir(SettingsCache::instance().paths().getPicsPath()).exists() ||
|
||||
SettingsCache::instance().paths().getPicsPath().isEmpty()) {
|
||||
if (QMessageBox::critical(this, QCoreApplication::translate("DlgSettings", "Error"),
|
||||
QCoreApplication::translate("DlgSettings",
|
||||
"The path to your card pictures directory is invalid. "
|
||||
"Would you like to go back and set the correct path?"),
|
||||
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return close();
|
||||
}
|
||||
46
cockatrice/src/interface/widgets/tabs/tab_settings.h
Normal file
46
cockatrice/src/interface/widgets/tabs/tab_settings.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* @file tab_settings.h
|
||||
* @brief Settings tab hosting the shared SettingsView
|
||||
* @ingroup Tabs
|
||||
*/
|
||||
#ifndef TAB_SETTINGS_H
|
||||
#define TAB_SETTINGS_H
|
||||
|
||||
#include "tab.h"
|
||||
|
||||
#include <QString>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class SettingsView;
|
||||
class TabSupervisor;
|
||||
|
||||
/**
|
||||
* @brief Singleton settings tab owned by the TabSupervisor
|
||||
*
|
||||
* Thin Tab frame around SettingsView: page selection, tab title and the close
|
||||
* validation live here. All settings content is owned by SettingsView.
|
||||
*/
|
||||
class TabSettings : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
private:
|
||||
SettingsView *settingsView;
|
||||
|
||||
public:
|
||||
explicit TabSettings(TabSupervisor *_tabSupervisor, int pageIndex = -1);
|
||||
/** @brief Selects the given SettingsView page (range-checked, no-op when out of range) */
|
||||
void setTab(int pageIndex);
|
||||
/** @brief The hosted settings content */
|
||||
[[nodiscard]] SettingsView *getSettingsView() const
|
||||
{
|
||||
return settingsView;
|
||||
}
|
||||
void retranslateUi() override;
|
||||
[[nodiscard]] QString getTabText() const override
|
||||
{
|
||||
return tr("Settings");
|
||||
}
|
||||
bool closeRequest() override;
|
||||
};
|
||||
|
||||
#endif // TAB_SETTINGS_H
|
||||
|
|
@ -39,6 +39,7 @@
|
|||
#include "tab_report.h"
|
||||
#include "tab_room.h"
|
||||
#include "tab_server.h"
|
||||
#include "tab_settings.h"
|
||||
#include "tab_visual_database_display.h"
|
||||
#include "user_level.h"
|
||||
#include "visual_deck_editor/tab_deck_editor_visual.h"
|
||||
|
|
@ -154,9 +155,9 @@ void CloseButton::paintEvent(QPaintEvent * /*event*/)
|
|||
|
||||
TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent)
|
||||
: QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabHome(nullptr),
|
||||
tabVisualDeckStorage(nullptr), tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr),
|
||||
tabReplays(nullptr), tabAdmin(nullptr), tabCardArtRules(nullptr), tabLog(nullptr), tabReport(nullptr),
|
||||
tabModeration(nullptr), tabDeveloper(nullptr), isLocalGame(false)
|
||||
tabVisualDeckStorage(nullptr), tabServer(nullptr), tabSettings(nullptr), tabAccount(nullptr),
|
||||
tabDeckStorage(nullptr), tabReplays(nullptr), tabAdmin(nullptr), tabCardArtRules(nullptr), tabLog(nullptr),
|
||||
tabReport(nullptr), tabModeration(nullptr), tabDeveloper(nullptr), isLocalGame(false)
|
||||
{
|
||||
setElideMode(Qt::ElideRight);
|
||||
setMovable(true);
|
||||
|
|
@ -293,6 +294,7 @@ void TabSupervisor::retranslateUi()
|
|||
// tabs
|
||||
QList<Tab *> tabs;
|
||||
tabs.append(tabServer);
|
||||
tabs.append(tabSettings);
|
||||
tabs.append(tabReplays);
|
||||
tabs.append(tabDeckStorage);
|
||||
tabs.append(tabAdmin);
|
||||
|
|
@ -753,6 +755,25 @@ void TabSupervisor::openTabServer()
|
|||
aTabServer->setChecked(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Opens the singleton settings tab, creating it on first use
|
||||
*
|
||||
* @param pageIndex Initial SettingsView page, or -1 to keep the current page
|
||||
* @return The settings tab
|
||||
*/
|
||||
TabSettings *TabSupervisor::openTabSettings(int pageIndex)
|
||||
{
|
||||
if (!tabSettings) {
|
||||
tabSettings = new TabSettings(this, pageIndex);
|
||||
myAddTab(tabSettings, nullptr);
|
||||
connect(tabSettings, &QObject::destroyed, this, [this] { tabSettings = nullptr; });
|
||||
} else if (pageIndex >= 0) {
|
||||
tabSettings->setTab(pageIndex);
|
||||
}
|
||||
setCurrentWidget(tabSettings);
|
||||
return tabSettings;
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabAccount(bool checked)
|
||||
{
|
||||
SettingsCache::instance().tabs().setTabAccountOpen(checked);
|
||||
|
|
@ -919,6 +940,7 @@ void TabSupervisor::openTabReport()
|
|||
myAddTab(tabReport, aTabReport);
|
||||
connect(tabReport, &TabReport::openReplay, this, &TabSupervisor::openReplay);
|
||||
connect(tabReport, &TabReport::requestJoinGame, this, &TabSupervisor::joinReportGame);
|
||||
connect(tabReport, &TabReport::openMessageDialog, this, &TabSupervisor::addMessageTab);
|
||||
connect(tabReport, &QObject::destroyed, this, [this] {
|
||||
tabReport = nullptr;
|
||||
aTabReport->setChecked(false);
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class UserListManager;
|
|||
class AbstractClient;
|
||||
class Tab;
|
||||
class TabServer;
|
||||
class TabSettings;
|
||||
class TabRoom;
|
||||
class TabHome;
|
||||
class TabGame;
|
||||
|
|
@ -108,6 +109,7 @@ private:
|
|||
TabHome *tabHome;
|
||||
TabDeckStorageVisual *tabVisualDeckStorage;
|
||||
TabServer *tabServer;
|
||||
TabSettings *tabSettings;
|
||||
TabAccount *tabAccount;
|
||||
TabDeckStorage *tabDeckStorage;
|
||||
TabReplays *tabReplays;
|
||||
|
|
@ -214,6 +216,7 @@ public slots:
|
|||
void actTabVisualDeckStorage(bool checked);
|
||||
void actTabReplays(bool checked);
|
||||
void openTabServer();
|
||||
TabSettings *openTabSettings(int pageIndex = -1);
|
||||
void addRoomTab(const ServerInfo_Room &info, bool setCurrent);
|
||||
void openTabPublicDecks(const QString &userName);
|
||||
private slots:
|
||||
|
|
|
|||
|
|
@ -130,10 +130,19 @@ void LineEditCompleter::keyPressEvent(QKeyEvent *event)
|
|||
CompleterInfo *active = nullptr;
|
||||
QString prefix;
|
||||
|
||||
// An open "[[" card link takes precedence over a mention: while the cursor is
|
||||
// inside it, an earlier "@" must not shadow the card completer.
|
||||
const int openCardPos = textValue.lastIndexOf("[[", cursorPos - 1);
|
||||
const int closeCardPos = openCardPos == -1 ? -1 : textValue.indexOf("]]", openCardPos + 2);
|
||||
const bool insideCardLink = openCardPos != -1 && (closeCardPos == -1 || closeCardPos >= cursorPos);
|
||||
|
||||
for (auto &info : completers) {
|
||||
bool triggered = false;
|
||||
switch (info.trigger) {
|
||||
case CompleterTrigger::Mention: {
|
||||
if (insideCardLink) {
|
||||
break;
|
||||
}
|
||||
int triggerPos = textValue.lastIndexOf("@", cursorPos - 1);
|
||||
if (triggerPos != -1 && (triggerPos == 0 || textValue[triggerPos - 1].isSpace())) {
|
||||
triggered = true;
|
||||
|
|
|
|||
|
|
@ -100,10 +100,27 @@ void renderReportDetails(QTextEdit *chatLogEdit,
|
|||
const QString &moderatorPrefix,
|
||||
const QString &nonModeratorPrefix)
|
||||
{
|
||||
QString details;
|
||||
if (report.has_resolution_note() && !report.resolution_note().empty()) {
|
||||
details = QObject::tr("Resolution note: %1").arg(QString::fromStdString(report.resolution_note()));
|
||||
}
|
||||
if (report.has_resolution_time()) {
|
||||
QString resolvedBy = QObject::tr("unknown");
|
||||
if (report.has_resolved_by_name() && !report.resolved_by_name().empty()) {
|
||||
resolvedBy = QString::fromStdString(report.resolved_by_name());
|
||||
}
|
||||
if (!details.isEmpty()) {
|
||||
details += "\n";
|
||||
}
|
||||
details += QObject::tr("Resolved at %1 by %2").arg(formatReportTime(report.resolution_time()), resolvedBy);
|
||||
}
|
||||
|
||||
if (report.has_chat_log() && !report.chat_log().empty()) {
|
||||
chatLogEdit->setPlainText(QString::fromStdString(report.chat_log()));
|
||||
const QString chatLog = QString::fromStdString(report.chat_log());
|
||||
chatLogEdit->setPlainText(
|
||||
details.isEmpty() ? chatLog : details + "\n\n" + QObject::tr("=== Chat Log ===") + "\n" + chatLog);
|
||||
} else {
|
||||
chatLogEdit->clear();
|
||||
chatLogEdit->setPlainText(details);
|
||||
}
|
||||
|
||||
commentsEdit->clear();
|
||||
|
|
|
|||
|
|
@ -26,14 +26,15 @@
|
|||
#include "../interface/widgets/dialogs/dlg_edit_tokens.h"
|
||||
#include "../interface/widgets/dialogs/dlg_local_game_options.h"
|
||||
#include "../interface/widgets/dialogs/dlg_manage_sets.h"
|
||||
#include "../interface/widgets/dialogs/dlg_settings.h"
|
||||
#include "../interface/widgets/dialogs/dlg_startup_card_check.h"
|
||||
#include "../interface/widgets/dialogs/dlg_tip_of_the_day.h"
|
||||
#include "../interface/widgets/dialogs/dlg_update.h"
|
||||
#include "../interface/widgets/dialogs/dlg_view_log.h"
|
||||
#include "../interface/widgets/dialogs/settings_view.h"
|
||||
#include "../interface/widgets/onboarding/first_run_wizard.h"
|
||||
#include "../interface/widgets/settings_page/general_settings_page.h"
|
||||
#include "../interface/widgets/tabs/tab_server.h"
|
||||
#include "../interface/widgets/tabs/tab_settings.h"
|
||||
#include "../interface/widgets/tabs/tab_supervisor.h"
|
||||
#include "../main.h"
|
||||
#include "abstract_client.h"
|
||||
|
|
@ -58,7 +59,9 @@
|
|||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QButtonGroup>
|
||||
#include <QCheckBox>
|
||||
#include <QCloseEvent>
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDesktopServices>
|
||||
|
|
@ -79,10 +82,12 @@
|
|||
#include <QPixmapCache>
|
||||
#include <QPushButton>
|
||||
#include <QSharedPointer>
|
||||
#include <QSize>
|
||||
#include <QStatusBar>
|
||||
#include <QStringLiteral>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <QTimer>
|
||||
#include <QToolButton>
|
||||
#include <QWidget>
|
||||
#include <QtConcurrentRun>
|
||||
#include <QtGlobal>
|
||||
|
|
@ -269,10 +274,13 @@ void MainWindow::actFullScreen(bool checked)
|
|||
|
||||
void MainWindow::actSettings()
|
||||
{
|
||||
DlgSettings dlg(this);
|
||||
auto *generalPage = qobject_cast<GeneralSettingsPage *>(dlg.page(DlgSettings::GeneralPage));
|
||||
connect(generalPage, &GeneralSettingsPage::cardDatabaseUpdateRequested, this, &MainWindow::actCheckCardUpdates);
|
||||
dlg.exec();
|
||||
TabSettings *settingsTab = tabSupervisor->openTabSettings();
|
||||
auto *generalPage =
|
||||
qobject_cast<GeneralSettingsPage *>(settingsTab->getSettingsView()->page(SettingsView::GeneralPage));
|
||||
if (generalPage) {
|
||||
connect(generalPage, &GeneralSettingsPage::cardDatabaseUpdateRequested, this, &MainWindow::actCheckCardUpdates,
|
||||
Qt::UniqueConnection);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::actExit()
|
||||
|
|
@ -352,6 +360,8 @@ void MainWindow::retranslateUi()
|
|||
aForgotPassword->setText(tr("&Restore password..."));
|
||||
aSettings->setText(tr("&Settings..."));
|
||||
aSettings->setIcon(themePixmap(QStringLiteral("icons/settings")));
|
||||
settingsButton->setToolTip(tr("Settings"));
|
||||
settingsButton->setIcon(themePixmap(QStringLiteral("icons/settings")));
|
||||
aExit->setText(tr("&Exit"));
|
||||
|
||||
#if defined(__APPLE__) /* For OSX */
|
||||
|
|
@ -556,6 +566,13 @@ MainWindow::MainWindow(QWidget *parent)
|
|||
|
||||
setCentralWidget(tabSupervisor);
|
||||
|
||||
// Subtle settings entry in the tab bar corner, mirroring the menu action
|
||||
settingsButton = new QToolButton(tabSupervisor);
|
||||
settingsButton->setAutoRaise(true);
|
||||
settingsButton->setIconSize(QSize(20, 20));
|
||||
connect(settingsButton, &QToolButton::clicked, this, &MainWindow::actSettings);
|
||||
tabSupervisor->setCornerWidget(settingsButton, Qt::TopRightCorner);
|
||||
|
||||
retranslateUi();
|
||||
|
||||
if (!restoreGeometry(SettingsCache::instance().layouts().getMainWindowGeometry())) {
|
||||
|
|
@ -603,8 +620,39 @@ MainWindow::MainWindow(QWidget *parent)
|
|||
QTimer::singleShot(0, this, &MainWindow::applyStartupDestination);
|
||||
}
|
||||
|
||||
void MainWindow::warnAboutDataPathsInProgramDir()
|
||||
{
|
||||
auto &paths = SettingsCache::instance().paths();
|
||||
if (SettingsCache::instance().getIsPortableBuild() || paths.getAppDirWarningAcknowledged()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QStringList offenders = paths.pathsInsideDir(QCoreApplication::applicationDirPath());
|
||||
if (offenders.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox box(QMessageBox::Warning, tr("Data paths inside the program directory"),
|
||||
tr("These paths are configured inside the Cockatrice program directory:\n\n%1\n\n"
|
||||
"Cockatrice keeps its own data outside the program directory by default. Files "
|
||||
"stored inside it can be deleted by an update or by uninstalling Cockatrice. "
|
||||
"Change them in the Settings dialog - most are on the General page - to keep "
|
||||
"them safe.")
|
||||
.arg(offenders.join(QLatin1Char('\n'))),
|
||||
QMessageBox::Ok, this);
|
||||
box.setTextFormat(Qt::PlainText);
|
||||
auto *dontShowAgain = new QCheckBox(tr("Don't show this again"));
|
||||
box.setCheckBox(dontShowAgain);
|
||||
box.exec();
|
||||
if (dontShowAgain->isChecked()) {
|
||||
paths.setAppDirWarningAcknowledged(true);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::startupConfigCheck()
|
||||
{
|
||||
warnAboutDataPathsInProgramDir();
|
||||
|
||||
const bool isCleanInstall = SettingsCache::instance().network().getClientVersion() == CLIENT_INFO_NOT_SET;
|
||||
|
||||
// checkUnknownSets() is intentionally deferred from the card database load
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
#include <qtmetamacros.h>
|
||||
|
||||
class QAction;
|
||||
class QToolButton;
|
||||
class QWidget;
|
||||
struct LocalGameOptions;
|
||||
|
||||
|
|
@ -149,6 +150,9 @@ private:
|
|||
|
||||
void runFirstRunWizard(bool firstRun = false);
|
||||
|
||||
/** @brief Warns once when a non-portable install keeps its data paths inside the program directory. */
|
||||
void warnAboutDataPathsInProgramDir();
|
||||
|
||||
inline QString getCardUpdaterBinaryName()
|
||||
{
|
||||
return "oracle";
|
||||
|
|
@ -170,6 +174,7 @@ private:
|
|||
QList<QMenu *> tabMenus;
|
||||
QMenu *cockatriceMenu, *dbMenu, *tabsMenu, *helpMenu, *trayIconMenu;
|
||||
QAction *aAbout, *aSettings, *aShow, *aExit;
|
||||
QToolButton *settingsButton = nullptr;
|
||||
QAction *aConnect, *aDisconnect, *aRegister, *aForgotPassword, *aSinglePlayer, *aWatchReplay, *aFullScreen;
|
||||
QAction *aManageSets, *aEditTokens, *aOpenCustomFolder, *aOpenCustomsetsFolder, *aAddCustomSet,
|
||||
*aReloadCardDatabase;
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ suffix, e.g. `card_picture_loader.*`, to include all sub categories).
|
|||
| User interface and themes | `theme_manager`, `sound_engine`, `flow_layout`, `flow_widget.*`, `pixel_map_generator`, `card_info_picture_widget` |
|
||||
| Networking and servers | `local_client`, `remote_client`, `tapped_out_interface`, `servers_settings` |
|
||||
| In-game logic | `player`, `game_scene.*`, `card_zone.*`, `view_zone`, `game_event_handler` |
|
||||
| Dialogs and tabs | `dlg_settings`, `dlg_update`, `dlg_tip_of_the_day`, `tab_game`, `tab_message`, `tab_supervisor` |
|
||||
| Dialogs and tabs | `tab_settings`, `dlg_update`, `dlg_tip_of_the_day`, `tab_game`, `tab_message`, `tab_supervisor` |
|
||||
| Settings and shortcuts | `settings_cache`, `shortcuts_settings` |
|
||||
| Deck and card filtering | `filter_string`, `deck_filter_string`, `syntax_help` |
|
||||
|
||||
|
|
|
|||
63
gcovr.cfg
Normal file
63
gcovr.cfg
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# gcovr configuration for Cockatrice line coverage.
|
||||
#
|
||||
# Used by .ci/compile.sh --coverage and by hand:
|
||||
#
|
||||
# cmake -S . -B build/cover -DCMAKE_BUILD_TYPE=Debug -DWITH_SERVER=1 -DTEST=1 -DENABLE_COVERAGE=ON
|
||||
# cmake --build build/cover -j 24
|
||||
# ctest --test-dir build/cover -LE bench -j 24
|
||||
# gcovr build/cover --root . --object-directory build/cover --txt --decisions
|
||||
#
|
||||
# The point of most of this file is subtraction. A raw report is dominated by code no test can
|
||||
# reach: vendored third-party trees, build directories left over from earlier experiments, and
|
||||
# the client's main() and window classes, which have no headless test. Those all dilute the one
|
||||
# number worth watching, so they are filtered out here rather than in every invocation.
|
||||
|
||||
# root is deliberately not set here. This file is normally invoked from the build directory
|
||||
# (compile.sh runs there, because it configures with `cmake ..`), so a `root` of `.` would
|
||||
# resolve to the build tree and every filter below would exclude everything. compile.sh passes
|
||||
# --root explicitly instead.
|
||||
|
||||
# Our own code only. This keeps generated Qt MOC output, protobuf output and anything CMake
|
||||
# built under the source tree out of the report.
|
||||
filter = ^cockatrice/
|
||||
filter = ^libcockatrice_[^/]+/
|
||||
filter = ^servatrice/
|
||||
filter = ^oracle/
|
||||
|
||||
# The tests are the vehicle, not the subject. They are compiled with the same instrumentation,
|
||||
# so leaving them in would add their near-perfect coverage to the numerator and quietly report a
|
||||
# healthier project than we have: test code is only ever executed by the run that is measuring
|
||||
# it. Helpers under tests/ are excluded on the same grounds.
|
||||
exclude = ^tests/
|
||||
|
||||
# Vendored third-party code is not ours to cover and is never going to move the number.
|
||||
exclude = ^vcpkg/
|
||||
exclude = ^doc/doxygen/theme/
|
||||
exclude = ^libcockatrice_rng/libcockatrice/rng/sfmt/
|
||||
exclude = ^libcockatrice_utility/libcockatrice/utility/peglib\.h$
|
||||
exclude = ^oracle/src/lzma/
|
||||
exclude = ^oracle/src/qt-json/
|
||||
exclude = ^oracle/src/zip/
|
||||
exclude = ^servatrice/src/smtp/
|
||||
|
||||
# Qt's own moc/pch/uic output is generated into the build tree, not the source tree, so the
|
||||
# filters above already keep it out. These cover CLion's directories, which sit in-tree and are
|
||||
# picked up by the object-directory walk.
|
||||
exclude = .*cmake-build-.*
|
||||
exclude = .*\.cache/
|
||||
|
||||
# Process entry points and the client's window chrome. Each of these needs a display, an
|
||||
# interactive terminal, or a live database before it does anything a test could observe, so no
|
||||
# headless run will ever move them. Left in, they contribute thousands of lines at 0% and drag
|
||||
# the total down by a fixed amount that hides movement in the code we can actually test. The
|
||||
# window header is excluded with its .cpp because gcovr attributes its inline template code
|
||||
# there, which is where most of its line count lives.
|
||||
exclude = ^cockatrice/src/main\.cpp$
|
||||
exclude = ^cockatrice/src/interface/window_main\.
|
||||
exclude = ^oracle/src/main\.cpp$
|
||||
exclude = ^servatrice/src/main\.cpp$
|
||||
|
||||
exclude-throw-branches = yes
|
||||
exclude-function-lines = yes
|
||||
|
||||
print-summary = yes
|
||||
|
|
@ -60,3 +60,5 @@ target_link_libraries(
|
|||
PUBLIC libcockatrice_utility
|
||||
PUBLIC ${QT_CORE_MODULE}
|
||||
)
|
||||
|
||||
target_precompile_headers(libcockatrice_card PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||
|
|
|
|||
|
|
@ -41,3 +41,5 @@ target_link_libraries(
|
|||
libcockatrice_deck_list PUBLIC libcockatrice_interfaces libcockatrice_protocol libcockatrice_utility
|
||||
${QT_CORE_MODULE}
|
||||
)
|
||||
|
||||
target_precompile_headers(libcockatrice_deck_list PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||
|
|
|
|||
|
|
@ -18,3 +18,5 @@ add_dependencies(libcockatrice_filters libcockatrice_card)
|
|||
target_include_directories(libcockatrice_filters PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
target_link_libraries(libcockatrice_filters PUBLIC libcockatrice_card ${QT_CORE_MODULE})
|
||||
|
||||
target_precompile_headers(libcockatrice_filters PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||
|
|
|
|||
|
|
@ -31,3 +31,5 @@ add_library(libcockatrice_interfaces STATIC ${MOC_SOURCES})
|
|||
target_include_directories(libcockatrice_interfaces PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
target_link_libraries(libcockatrice_interfaces PUBLIC libcockatrice_utility ${QT_CORE_MODULE})
|
||||
|
||||
target_precompile_headers(libcockatrice_interfaces PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||
|
|
|
|||
|
|
@ -29,3 +29,5 @@ add_library(
|
|||
target_include_directories(libcockatrice_models_database PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
target_link_libraries(libcockatrice_models_database PUBLIC libcockatrice_card libcockatrice_filters ${QT_CORE_MODULE})
|
||||
|
||||
target_precompile_headers(libcockatrice_models_database PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||
|
|
|
|||
|
|
@ -16,3 +16,5 @@ target_include_directories(libcockatrice_models_deck_list PUBLIC ${CMAKE_CURRENT
|
|||
target_link_libraries(
|
||||
libcockatrice_models_deck_list PUBLIC libcockatrice_card libcockatrice_deck_list ${QT_CORE_MODULE}
|
||||
)
|
||||
|
||||
target_precompile_headers(libcockatrice_models_deck_list PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||
|
|
|
|||
|
|
@ -19,3 +19,5 @@ target_link_libraries(
|
|||
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets libcockatrice_protocol
|
||||
libcockatrice_network_server_remote
|
||||
)
|
||||
|
||||
target_precompile_headers(libcockatrice_network_client_abstract PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||
|
|
|
|||
|
|
@ -19,3 +19,5 @@ target_link_libraries(
|
|||
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets
|
||||
libcockatrice_network_client_abstract
|
||||
)
|
||||
|
||||
target_precompile_headers(libcockatrice_network_client_local PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||
|
|
|
|||
|
|
@ -19,3 +19,5 @@ target_link_libraries(
|
|||
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets
|
||||
libcockatrice_network_client_abstract libcockatrice_interfaces libcockatrice_utility libcockatrice_protocol
|
||||
)
|
||||
|
||||
target_precompile_headers(libcockatrice_network_client_remote PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||
|
|
|
|||
|
|
@ -15,3 +15,5 @@ add_dependencies(libcockatrice_network_server_local libcockatrice_protocol)
|
|||
target_include_directories(libcockatrice_network_server_local PUBLIC .)
|
||||
|
||||
target_link_libraries(libcockatrice_network_server_local PUBLIC ${COCKATRICE_QT_MODULES} libcockatrice_protocol)
|
||||
|
||||
target_precompile_headers(libcockatrice_network_server_local PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||
|
|
|
|||
|
|
@ -62,3 +62,5 @@ target_link_libraries(
|
|||
libcockatrice_network_server_remote PUBLIC libcockatrice_protocol libcockatrice_utility libcockatrice_rng
|
||||
libcockatrice_deck_list ${COCKATRICE_QT_MODULES}
|
||||
)
|
||||
|
||||
target_precompile_headers(libcockatrice_network_server_remote PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||
|
|
|
|||
|
|
@ -56,6 +56,10 @@ public:
|
|||
{
|
||||
return deck;
|
||||
}
|
||||
bool getSideboardLocked() const
|
||||
{
|
||||
return sideboardLocked;
|
||||
}
|
||||
bool getReadyStart() const
|
||||
{
|
||||
return readyStart;
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@
|
|||
#include <QTimer>
|
||||
#include <google/protobuf/descriptor.h>
|
||||
#include <google/protobuf/message.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/protocol/pb/context_deck_select.pb.h>
|
||||
#include <libcockatrice/protocol/pb/context_ping_changed.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_delete_arrow.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_game_closed.pb.h>
|
||||
|
|
@ -825,6 +827,48 @@ void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParti
|
|||
}
|
||||
|
||||
rc.enqueuePostResponseItem(ServerMessage::GAME_EVENT_CONTAINER, prepareGameEvent(event2, -1));
|
||||
|
||||
// A newly joined player never saw the "Player X has loaded a deck" messages
|
||||
// (they are only broadcast when a deck is selected), so replay a deck-select
|
||||
// context for every player that already has a deck loaded. It is delivered
|
||||
// privately to the joining participant, whose client renders it as a log
|
||||
// entry in the same way as a fresh deck selection. Skipped on reconnect
|
||||
// (resuming) — the player already saw these — and once the game has started,
|
||||
// when the message is no longer useful and would only spam the log.
|
||||
if (!resuming && !gameStarted) {
|
||||
for (auto *participant : participants.values()) {
|
||||
if (participant == joiningParticipant) {
|
||||
continue;
|
||||
}
|
||||
auto *deckOwner = qobject_cast<Server_Player *>(participant);
|
||||
if (deckOwner == nullptr) {
|
||||
continue;
|
||||
}
|
||||
const DeckList *deckList = deckOwner->getDeckList();
|
||||
if (deckList == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Event_PlayerPropertiesChanged event;
|
||||
event.mutable_player_properties()->set_sideboard_locked(deckOwner->getSideboardLocked());
|
||||
event.mutable_player_properties()->set_deck_hash(deckList->getDeckHash().toStdString());
|
||||
|
||||
Context_DeckSelect deckSelect;
|
||||
deckSelect.set_deck_hash(deckList->getDeckHash().toStdString());
|
||||
deckSelect.set_sideboard_size(deckList->getSideboardSize());
|
||||
if (getShareDecklistsOnLoad()) {
|
||||
deckSelect.set_deck_list(deckList->writeToString_Native().toStdString());
|
||||
}
|
||||
|
||||
GameEventContext context;
|
||||
context.GetReflection()
|
||||
->MutableMessage(&context, deckSelect.GetDescriptor()->FindExtensionByName("ext"))
|
||||
->CopyFrom(deckSelect);
|
||||
|
||||
rc.enqueuePostResponseItem(ServerMessage::GAME_EVENT_CONTAINER,
|
||||
prepareGameEvent(event, deckOwner->getPlayerId(), &context));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Server_Game::sendGameEventContainer(GameEventContainer *cont,
|
||||
|
|
|
|||
|
|
@ -24,3 +24,5 @@ target_include_directories(
|
|||
libcockatrice_protocol PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR} # points to the generated headers
|
||||
)
|
||||
|
||||
target_precompile_headers(libcockatrice_protocol PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ set(PROTO_FILES
|
|||
command_report_details.proto
|
||||
command_report_list.proto
|
||||
command_report_my_list.proto
|
||||
command_report_reopen.proto
|
||||
command_report_resolve.proto
|
||||
command_report_stats.proto
|
||||
command_report_user_info.proto
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
syntax = "proto2";
|
||||
import "moderator_commands.proto";
|
||||
|
||||
message Command_ReportReopen {
|
||||
extend ModeratorCommand {
|
||||
optional Command_ReportReopen ext = 1206;
|
||||
}
|
||||
required int32 report_id = 1;
|
||||
optional string reopen_note = 2;
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ message ModeratorCommand {
|
|||
REPLAY_DOWNLOAD_BY_GAME_ID = 1203;
|
||||
REPORT_USER_INFO = 1204;
|
||||
REPORT_STATS = 1205;
|
||||
REPORT_REOPEN = 1206;
|
||||
}
|
||||
extensions 100 to max;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,4 +33,7 @@ message ServerInfo_Report {
|
|||
optional string chat_log = 14;
|
||||
|
||||
optional string resolution_note = 15;
|
||||
optional string resolved_by_name = 16;
|
||||
|
||||
optional int32 reported_user_level = 17;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ add_library(
|
|||
libcockatrice/rng/sfmt/SFMT.c
|
||||
)
|
||||
|
||||
# SFMT is vendored and its own headers trip UBSan's alignment and vptr checks.
|
||||
cockatrice_disable_sanitizers_for(libcockatrice/rng/sfmt/SFMT.c)
|
||||
cockatrice_disable_coverage_for(libcockatrice/rng/sfmt/SFMT.c)
|
||||
|
||||
target_include_directories(libcockatrice_rng PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
target_link_libraries(libcockatrice_rng PUBLIC ${QT_CORE_MODULE})
|
||||
|
|
|
|||
|
|
@ -71,3 +71,5 @@ target_include_directories(
|
|||
)
|
||||
|
||||
target_link_libraries(libcockatrice_settings PUBLIC libcockatrice_card libcockatrice_utility ${QT_CORE_MODULE})
|
||||
|
||||
target_precompile_headers(libcockatrice_settings PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
#include "libcockatrice/settings/settings_manager.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QStringList>
|
||||
#include <QVariant>
|
||||
|
||||
class QObject;
|
||||
|
|
@ -121,3 +123,52 @@ void PathsSettings::setSpoilerDatabasePath(const QString &_spoilerDatabasePath)
|
|||
setValue(_spoilerDatabasePath, "spoilerDatabase");
|
||||
emit cardDatabasePathChanged();
|
||||
}
|
||||
|
||||
QStringList PathsSettings::pathsInsideDir(const QString &baseDir) const
|
||||
{
|
||||
QStringList offenders;
|
||||
const auto check = [&offenders, &baseDir](const QString &path) {
|
||||
if (!path.isEmpty() && isInsideDir(path, baseDir)) {
|
||||
offenders.append(QDir::cleanPath(path));
|
||||
}
|
||||
};
|
||||
|
||||
check(getDeckPath());
|
||||
check(getFiltersPath());
|
||||
check(getReplaysPath());
|
||||
check(getPicsPath());
|
||||
check(getCustomPicsPath());
|
||||
check(getThemesPath());
|
||||
check(getCardDatabasePath());
|
||||
check(getCustomCardDatabasePath());
|
||||
check(getTokenDatabasePath());
|
||||
check(getSpoilerCardDatabasePath());
|
||||
check(getRedirectCachePath());
|
||||
|
||||
offenders.removeDuplicates();
|
||||
return offenders;
|
||||
}
|
||||
|
||||
bool PathsSettings::isInsideDir(const QString &path, const QString &baseDir)
|
||||
{
|
||||
#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
|
||||
// Windows and default macOS volumes are case-insensitive filesystems.
|
||||
constexpr Qt::CaseSensitivity caseSensitivity = Qt::CaseInsensitive;
|
||||
#else
|
||||
constexpr Qt::CaseSensitivity caseSensitivity = Qt::CaseSensitive;
|
||||
#endif
|
||||
const QString base = QDir::cleanPath(baseDir);
|
||||
const QString cleaned = QDir::cleanPath(path);
|
||||
const QString prefix = base.endsWith(QLatin1Char('/')) ? base : base + QLatin1Char('/');
|
||||
return cleaned == base || cleaned.startsWith(prefix, caseSensitivity);
|
||||
}
|
||||
|
||||
bool PathsSettings::getAppDirWarningAcknowledged() const
|
||||
{
|
||||
return getValue("appDirWarningAcknowledged").toBool();
|
||||
}
|
||||
|
||||
void PathsSettings::setAppDirWarningAcknowledged(bool _appDirWarningAcknowledged)
|
||||
{
|
||||
setValue(_appDirWarningAcknowledged, "appDirWarningAcknowledged");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "settings_manager.h"
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <libcockatrice/interfaces/interface_paths_settings_provider.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
|
|
@ -27,6 +28,24 @@ public:
|
|||
[[nodiscard]] QString getSpoilerCardDatabasePath() const override;
|
||||
[[nodiscard]] QString getRedirectCachePath() const override;
|
||||
|
||||
/**
|
||||
* @brief Returns the configured paths that resolve inside baseDir.
|
||||
*
|
||||
* Comparison is lexical (QDir::cleanPath); symlinks are not resolved, so a
|
||||
* path that only reaches baseDir through a symlink is not reported.
|
||||
*/
|
||||
[[nodiscard]] QStringList pathsInsideDir(const QString &baseDir) const;
|
||||
|
||||
/**
|
||||
* @brief Whether path resolves inside baseDir, as a single-path check.
|
||||
*
|
||||
* A path counts as inside when it equals baseDir or is nested below it;
|
||||
* a sibling sharing the name prefix (baseDir + "2") does not.
|
||||
*/
|
||||
[[nodiscard]] static bool isInsideDir(const QString &path, const QString &baseDir);
|
||||
|
||||
[[nodiscard]] bool getAppDirWarningAcknowledged() const;
|
||||
|
||||
void setDeckPath(const QString &_deckPath);
|
||||
void setFiltersPath(const QString &_filtersPath);
|
||||
void setReplaysPath(const QString &_replaysPath);
|
||||
|
|
@ -37,14 +56,17 @@ public:
|
|||
void setCustomCardDatabasePath(const QString &_customCardDatabasePath);
|
||||
void setTokenDatabasePath(const QString &_tokenDatabasePath);
|
||||
void setSpoilerDatabasePath(const QString &_spoilerDatabasePath);
|
||||
void setAppDirWarningAcknowledged(bool _appDirWarningAcknowledged);
|
||||
|
||||
signals:
|
||||
void cardDatabasePathChanged();
|
||||
void picsPathChanged();
|
||||
void themeChanged();
|
||||
|
||||
private:
|
||||
public:
|
||||
explicit PathsSettings(const QString &settingPath, QObject *parent = nullptr);
|
||||
|
||||
private:
|
||||
PathsSettings(const PathsSettings & /*other*/);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -49,3 +49,5 @@ target_link_libraries(libcockatrice_utility PUBLIC libcockatrice_rng OpenSSL::Cr
|
|||
set(ORACLE_LIBS)
|
||||
|
||||
include_directories(${${COCKATRICE_QT_VERSION_NAME}Core_INCLUDE_DIRS})
|
||||
|
||||
target_precompile_headers(libcockatrice_utility PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||
|
|
|
|||
|
|
@ -79,6 +79,9 @@ if(ZLIB_FOUND)
|
|||
include_directories(${ZLIB_INCLUDE_DIRS})
|
||||
add_definitions("-DHAS_ZLIB")
|
||||
list(APPEND oracle_SOURCES src/zip/unzip.cpp src/zip/zipglobal.cpp)
|
||||
# minizip and zlib are vendored.
|
||||
cockatrice_disable_sanitizers_for(src/zip/unzip.cpp src/zip/zipglobal.cpp)
|
||||
cockatrice_disable_coverage_for(src/zip/unzip.cpp src/zip/zipglobal.cpp)
|
||||
else()
|
||||
message(STATUS "Oracle: zlib not found; ZIP support disabled")
|
||||
endif()
|
||||
|
|
@ -89,6 +92,9 @@ if(LIBLZMA_FOUND)
|
|||
include_directories(${LIBLZMA_INCLUDE_DIRS})
|
||||
add_definitions("-DHAS_LZMA")
|
||||
list(APPEND oracle_SOURCES src/lzma/decompress.cpp)
|
||||
# The LZMA SDK is vendored.
|
||||
cockatrice_disable_sanitizers_for(src/lzma/decompress.cpp)
|
||||
cockatrice_disable_coverage_for(src/lzma/decompress.cpp)
|
||||
else()
|
||||
message(STATUS "Oracle: LibLZMA not found; xz support disabled")
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -27,6 +27,14 @@ set(servatrice_SOURCES
|
|||
|
||||
set(servatrice_RESOURCES servatrice.qrc)
|
||||
|
||||
# Qxt SMTP is vendored.
|
||||
cockatrice_disable_sanitizers_for(
|
||||
src/smtp/qxthmac.cpp src/smtp/qxtmailattachment.cpp src/smtp/qxtmailmessage.cpp src/smtp/qxtsmtp.cpp
|
||||
)
|
||||
cockatrice_disable_coverage_for(
|
||||
src/smtp/qxthmac.cpp src/smtp/qxtmailattachment.cpp src/smtp/qxtmailmessage.cpp src/smtp/qxtsmtp.cpp
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
set(servatrice_SOURCES ${servatrice_SOURCES} servatrice.rc)
|
||||
endif(WIN32)
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@
|
|||
#include <libcockatrice/protocol/pb/command_report_details.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_list.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_my_list.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_reopen.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_resolve.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_stats.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_user_info.pb.h>
|
||||
|
|
@ -434,6 +435,8 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedModeratorCo
|
|||
return cmdReportAssign(cmd.GetExtension(Command_ReportAssign::ext), rc);
|
||||
case ModeratorCommand::REPORT_RESOLVE:
|
||||
return cmdReportResolve(cmd.GetExtension(Command_ReportResolve::ext), rc);
|
||||
case ModeratorCommand::REPORT_REOPEN:
|
||||
return cmdReportReopen(cmd.GetExtension(Command_ReportReopen::ext), rc);
|
||||
case ModeratorCommand::VIEWLOG_HISTORY:
|
||||
return cmdGetLogHistory(cmd.GetExtension(Command_ViewLogHistory::ext), rc, true);
|
||||
case ModeratorCommand::GRANT_REPLAY_ACCESS:
|
||||
|
|
@ -1983,6 +1986,29 @@ Response::ResponseCode AbstractServerSocketInterface::cmdBanFromServer(const Com
|
|||
return Response::RespOk;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
int reportUserLevel(bool userExists, int adminFlags)
|
||||
{
|
||||
int userLevel = ServerInfo_User::IsUser;
|
||||
if (userExists) {
|
||||
userLevel |= ServerInfo_User::IsRegistered;
|
||||
if (adminFlags & 1) {
|
||||
userLevel |= ServerInfo_User::IsAdmin | ServerInfo_User::IsModerator;
|
||||
} else if (adminFlags & 2) {
|
||||
userLevel |= ServerInfo_User::IsModerator;
|
||||
}
|
||||
if (adminFlags & 4) {
|
||||
userLevel |= ServerInfo_User::IsJudge;
|
||||
}
|
||||
if (adminFlags & 8) {
|
||||
userLevel |= ServerInfo_User::IsDeveloper;
|
||||
}
|
||||
}
|
||||
return userLevel;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Response::ResponseCode AbstractServerSocketInterface::cmdReportList(const Command_ReportList &cmd,
|
||||
ResponseContainer &rc)
|
||||
{
|
||||
|
|
@ -1996,8 +2022,9 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportList(const Comman
|
|||
|
||||
// Columns: 0=id, 1=reporter_name, 2=reported_user_name, 3=game_id,
|
||||
// 4=category, 5=description, 6=created_at, 7=status,
|
||||
// 8=resolution_note, 9=assigned_mod_name,
|
||||
// 10=room_id, 11=replay_id
|
||||
// 8=resolution_note, 9=assigned_mod_name, 10=resolved_by_name,
|
||||
// 11=room_id, 12=replay_id,
|
||||
// 13=reported_user_id, 14=reported_user_admin
|
||||
QString whereClause;
|
||||
if (unresolvedOnly) {
|
||||
whereClause = "WHERE r.status = 'open' OR r.status = 'assigned' ";
|
||||
|
|
@ -2012,11 +2039,13 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportList(const Comman
|
|||
|
||||
QString queryStr = "SELECT r.id, r.reporter_name, r.reported_user_name, r.game_id, "
|
||||
"r.category, r.description, r.created_at, r.status, "
|
||||
"r.resolution_note, u.name AS assigned_mod_name, r.room_id, "
|
||||
"r.resolution_note, u.name AS assigned_mod_name, ru.name AS resolved_by_name, r.room_id, "
|
||||
"(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) "
|
||||
"AS replay_id "
|
||||
"AS replay_id, u2.id AS reported_user_id, u2.admin AS reported_user_admin "
|
||||
"FROM {prefix}_reports r "
|
||||
"LEFT JOIN {prefix}_users u ON r.assigned_to = u.id ";
|
||||
"LEFT JOIN {prefix}_users u ON r.assigned_to = u.id "
|
||||
"LEFT JOIN {prefix}_users ru ON r.resolved_by = ru.id "
|
||||
"LEFT JOIN {prefix}_users u2 ON u2.name = r.reported_user_name ";
|
||||
|
||||
if (unresolvedOnly) {
|
||||
queryStr += "WHERE r.status = 'open' OR r.status = 'assigned' ";
|
||||
|
|
@ -2057,14 +2086,19 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportList(const Comman
|
|||
if (!query->value(9).isNull()) {
|
||||
info->set_assigned_mod_name(query->value(9).toString().toStdString());
|
||||
}
|
||||
|
||||
if (!query->value(10).isNull()) {
|
||||
info->set_room_id(query->value(10).toInt());
|
||||
info->set_resolved_by_name(query->value(10).toString().toStdString());
|
||||
}
|
||||
|
||||
if (!query->value(11).isNull()) {
|
||||
info->set_replay_id(query->value(11).toInt());
|
||||
info->set_room_id(query->value(11).toInt());
|
||||
}
|
||||
|
||||
if (!query->value(12).isNull()) {
|
||||
info->set_replay_id(query->value(12).toInt());
|
||||
}
|
||||
|
||||
info->set_reported_user_level(reportUserLevel(!query->value(13).isNull(), query->value(14).toInt()));
|
||||
}
|
||||
|
||||
rc.setResponseExtension(re);
|
||||
|
|
@ -2737,11 +2771,13 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportMyList(const Comm
|
|||
|
||||
QString queryStr = "SELECT r.id, r.reporter_name, r.reported_user_name, r.game_id, "
|
||||
"r.category, r.description, r.created_at, r.status, "
|
||||
"r.resolution_note, u.name AS assigned_mod_name, r.room_id, "
|
||||
"r.resolution_note, u.name AS assigned_mod_name, ru.name AS resolved_by_name, r.room_id, "
|
||||
"(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) "
|
||||
"AS replay_id "
|
||||
"AS replay_id, u2.id AS reported_user_id, u2.admin AS reported_user_admin "
|
||||
"FROM {prefix}_reports r "
|
||||
"LEFT JOIN {prefix}_users u ON r.assigned_to = u.id "
|
||||
"LEFT JOIN {prefix}_users ru ON r.resolved_by = ru.id "
|
||||
"LEFT JOIN {prefix}_users u2 ON u2.name = r.reported_user_name "
|
||||
"WHERE r.reporter_id = :reporter_id "
|
||||
"ORDER BY r.created_at DESC LIMIT 200";
|
||||
|
||||
|
|
@ -2777,20 +2813,61 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportMyList(const Comm
|
|||
if (!query->value(9).isNull()) {
|
||||
info->set_assigned_mod_name(query->value(9).toString().toStdString());
|
||||
}
|
||||
|
||||
if (!query->value(10).isNull()) {
|
||||
info->set_room_id(query->value(10).toInt());
|
||||
info->set_resolved_by_name(query->value(10).toString().toStdString());
|
||||
}
|
||||
|
||||
if (!query->value(11).isNull()) {
|
||||
info->set_replay_id(query->value(11).toInt());
|
||||
info->set_room_id(query->value(11).toInt());
|
||||
}
|
||||
|
||||
if (!query->value(12).isNull()) {
|
||||
info->set_replay_id(query->value(12).toInt());
|
||||
}
|
||||
|
||||
info->set_reported_user_level(reportUserLevel(!query->value(13).isNull(), query->value(14).toInt()));
|
||||
}
|
||||
|
||||
rc.setResponseExtension(re);
|
||||
return Response::RespOk;
|
||||
}
|
||||
|
||||
Response::ResponseCode AbstractServerSocketInterface::cmdReportReopen(const Command_ReportReopen &cmd,
|
||||
ResponseContainer & /*rc*/)
|
||||
{
|
||||
if (!sqlInterface->checkSql()) {
|
||||
return Response::RespInternalError;
|
||||
}
|
||||
|
||||
int reportId = cmd.report_id();
|
||||
QString note = textFromStdString(cmd.reopen_note());
|
||||
|
||||
QSqlQuery *query = sqlInterface->prepareQuery("UPDATE {prefix}_reports "
|
||||
"SET status = 'open', resolution_note = NULL, "
|
||||
"resolution_time = NULL, resolved_by = NULL, assigned_to = NULL, "
|
||||
"notified = 0 "
|
||||
"WHERE id = :id AND status IN ('resolved', 'dismissed')");
|
||||
|
||||
query->bindValue(":id", reportId);
|
||||
|
||||
if (!sqlInterface->execSqlQuery(query)) {
|
||||
return Response::RespInternalError;
|
||||
}
|
||||
|
||||
if (query->numRowsAffected() == 0) {
|
||||
return Response::RespInvalidData;
|
||||
}
|
||||
|
||||
QString auditDetails = QString("Report #%1 reopened").arg(reportId);
|
||||
if (!note.isEmpty()) {
|
||||
auditDetails += QString(". Reason: %1").arg(note.left(200));
|
||||
}
|
||||
sqlInterface->addAuditRecord(QString::number(reportId), this->getAddress(),
|
||||
QString::fromStdString(userInfo->clientid()), "REPORT_REOPENED", auditDetails, true);
|
||||
|
||||
return Response::RespOk;
|
||||
}
|
||||
|
||||
Response::ResponseCode AbstractServerSocketInterface::cmdReportDetails(const Command_ReportDetails &cmd,
|
||||
ResponseContainer &rc)
|
||||
{
|
||||
|
|
@ -2806,17 +2883,19 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportDetails(const Com
|
|||
|
||||
// Columns: 0=id, 1=reporter_id, 2=reporter_name, 3=reported_user_name, 4=game_id,
|
||||
// 5=category, 6=description, 7=created_at, 8=status,
|
||||
// 9=resolution_note, 10=assigned_mod_name, 11=chat_log,
|
||||
// 12=room_id, 13=resolution_time, 14=replay_id
|
||||
QString queryStr = "SELECT r.id, r.reporter_id, r.reporter_name, r.reported_user_name, r.game_id, "
|
||||
"r.category, r.description, r.created_at, r.status, "
|
||||
"r.resolution_note, u.name AS assigned_mod_name, r.chat_log, r.room_id, "
|
||||
"r.resolution_time, "
|
||||
"(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) "
|
||||
"AS replay_id "
|
||||
"FROM {prefix}_reports r "
|
||||
"LEFT JOIN {prefix}_users u ON r.assigned_to = u.id "
|
||||
"WHERE r.id = :id";
|
||||
// 9=resolution_note, 10=assigned_mod_name, 11=resolved_by_name,
|
||||
// 12=chat_log, 13=room_id, 14=resolution_time, 15=replay_id
|
||||
QString queryStr =
|
||||
"SELECT r.id, r.reporter_id, r.reporter_name, r.reported_user_name, r.game_id, "
|
||||
"r.category, r.description, r.created_at, r.status, "
|
||||
"r.resolution_note, u.name AS assigned_mod_name, ru.name AS resolved_by_name, r.chat_log, r.room_id, "
|
||||
"r.resolution_time, "
|
||||
"(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) "
|
||||
"AS replay_id "
|
||||
"FROM {prefix}_reports r "
|
||||
"LEFT JOIN {prefix}_users u ON r.assigned_to = u.id "
|
||||
"LEFT JOIN {prefix}_users ru ON r.resolved_by = ru.id "
|
||||
"WHERE r.id = :id";
|
||||
QSqlQuery *query = sqlInterface->prepareQuery(queryStr);
|
||||
query->bindValue(":id", reportId);
|
||||
|
||||
|
|
@ -2855,21 +2934,24 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportDetails(const Com
|
|||
if (!query->value(10).isNull()) {
|
||||
info->set_assigned_mod_name(query->value(10).toString().toStdString());
|
||||
}
|
||||
|
||||
if (!query->value(11).isNull()) {
|
||||
info->set_chat_log(query->value(11).toString().toStdString());
|
||||
info->set_resolved_by_name(query->value(11).toString().toStdString());
|
||||
}
|
||||
|
||||
if (!query->value(12).isNull()) {
|
||||
info->set_room_id(query->value(12).toInt());
|
||||
info->set_chat_log(query->value(12).toString().toStdString());
|
||||
}
|
||||
|
||||
if (!query->value(13).isNull()) {
|
||||
info->set_resolution_time(query->value(13).toDateTime().toSecsSinceEpoch());
|
||||
info->set_room_id(query->value(13).toInt());
|
||||
}
|
||||
|
||||
if (!query->value(14).isNull()) {
|
||||
info->set_replay_id(query->value(14).toInt());
|
||||
info->set_resolution_time(query->value(14).toDateTime().toSecsSinceEpoch());
|
||||
}
|
||||
|
||||
if (!query->value(15).isNull()) {
|
||||
info->set_replay_id(query->value(15).toInt());
|
||||
}
|
||||
|
||||
QSqlQuery *commentQuery =
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ class Command_ReportAssign;
|
|||
class Command_ReportDetails;
|
||||
class Command_ReportList;
|
||||
class Command_ReportMyList;
|
||||
class Command_ReportReopen;
|
||||
class Command_ReportResolve;
|
||||
class Command_ReportStats;
|
||||
class Command_ReportUserInfo;
|
||||
|
|
@ -181,6 +182,7 @@ private:
|
|||
Response::ResponseCode cmdUpdateServerMessage(const Command_UpdateServerMessage &cmd, ResponseContainer &rc);
|
||||
Response::ResponseCode cmdReportAssign(const Command_ReportAssign &cmd, ResponseContainer &);
|
||||
Response::ResponseCode cmdReportResolve(const Command_ReportResolve &cmd, ResponseContainer &);
|
||||
Response::ResponseCode cmdReportReopen(const Command_ReportReopen &cmd, ResponseContainer &);
|
||||
Response::ResponseCode cmdReportUserInfo(const Command_ReportUserInfo &cmd, ResponseContainer &rc);
|
||||
Response::ResponseCode cmdReportStats(const Command_ReportStats &cmd, ResponseContainer &rc);
|
||||
Response::ResponseCode cmdRegisterAccount(const Command_Register &cmd, ResponseContainer &rc);
|
||||
|
|
|
|||
|
|
@ -2,109 +2,10 @@
|
|||
# If a new test needs additional Qt modules, add them there — not in individual test CMakeLists.txt files.
|
||||
enable_testing()
|
||||
|
||||
add_test(NAME dummy_test COMMAND dummy_test)
|
||||
add_test(NAME expression_test COMMAND expression_test)
|
||||
add_test(NAME clamped_arithmetic_test COMMAND clamped_arithmetic_test)
|
||||
add_test(NAME test_age_formatting COMMAND test_age_formatting)
|
||||
add_test(NAME password_hash_test COMMAND password_hash_test)
|
||||
add_test(NAME playmat_resolver_test COMMAND playmat_resolver_test)
|
||||
add_test(NAME server_card_counter_test COMMAND server_card_counter_test)
|
||||
add_test(NAME server_counter_test COMMAND server_counter_test)
|
||||
add_test(NAME server_rate_limiter_test COMMAND server_rate_limiter_test)
|
||||
add_test(NAME server_developer_role_test COMMAND server_developer_role_test)
|
||||
add_test(NAME server_game_join_test COMMAND server_game_join_test)
|
||||
add_test(NAME warning_categories_test COMMAND warning_categories_test)
|
||||
# The translation loader test compiles a .qm fixture, which needs lrelease from Qt Linguist.
|
||||
if(TARGET Qt6::lrelease)
|
||||
add_test(NAME translation_loader_test COMMAND translation_loader_test)
|
||||
endif()
|
||||
add_test(NAME report_categories_test COMMAND report_categories_test)
|
||||
add_test(NAME lag_monitor_test COMMAND lag_monitor_test)
|
||||
add_test(NAME latency_tracker_test COMMAND latency_tracker_test)
|
||||
add_test(NAME metrics_registry_test COMMAND metrics_registry_test)
|
||||
add_test(NAME card_update_progress_test COMMAND card_update_progress_test)
|
||||
add_test(NAME loader_local_matching_test COMMAND loader_local_matching_test)
|
||||
|
||||
add_test(NAME deck_hash_performance_test COMMAND deck_hash_performance_test)
|
||||
set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 15)
|
||||
include(GoogleTest)
|
||||
|
||||
# Find GTest
|
||||
|
||||
add_executable(dummy_test dummy_test.cpp)
|
||||
add_executable(expression_test expression_test.cpp)
|
||||
add_executable(clamped_arithmetic_test clamped_arithmetic_test.cpp)
|
||||
add_executable(test_age_formatting test_age_formatting.cpp)
|
||||
add_executable(password_hash_test password_hash_test.cpp)
|
||||
add_executable(playmat_resolver_test playmat_resolver_test.cpp)
|
||||
add_executable(deck_hash_performance_test deck_hash_performance_test.cpp)
|
||||
add_executable(server_card_counter_test server_card_counter_test.cpp)
|
||||
add_executable(server_counter_test server_counter_test.cpp)
|
||||
add_executable(server_rate_limiter_test server_rate_limiter_test.cpp)
|
||||
add_executable(server_developer_role_test server_developer_role_test.cpp)
|
||||
add_executable(server_game_join_test server_game_join_test.cpp)
|
||||
add_executable(warning_categories_test warning_categories_test.cpp)
|
||||
if(TARGET Qt6::lrelease)
|
||||
add_executable(translation_loader_test translation_loader_test.cpp)
|
||||
|
||||
# QTranslator only accepts real .qm files, so the translation loader test gets one compiled from its
|
||||
# own .ts fixture rather than a stub.
|
||||
set(TRANSLATION_LOADER_TEST_QM "${CMAKE_CURRENT_BINARY_DIR}/translation_loader_test_de.qm")
|
||||
add_custom_command(
|
||||
OUTPUT "${TRANSLATION_LOADER_TEST_QM}"
|
||||
COMMAND Qt6::lrelease "${CMAKE_CURRENT_SOURCE_DIR}/translation_loader_test.ts" -qm "${TRANSLATION_LOADER_TEST_QM}"
|
||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/translation_loader_test.ts"
|
||||
COMMENT "Compiling translation_loader_test.ts"
|
||||
)
|
||||
add_custom_target(translation_loader_test_qm DEPENDS "${TRANSLATION_LOADER_TEST_QM}")
|
||||
add_dependencies(translation_loader_test translation_loader_test_qm)
|
||||
target_compile_definitions(translation_loader_test PRIVATE TRANSLATION_LOADER_TEST_DIR="${CMAKE_CURRENT_BINARY_DIR}")
|
||||
|
||||
# A multi-config generator (MSBuild, Xcode) puts the executable in a per-config subdirectory while
|
||||
# the .qm stays in the current binary directory, so copy the fixture next to the binary: that is the
|
||||
# layout ABuildTreeFindsTheTranslationsNextToTheExecutable is about, and the only one it finds on
|
||||
# those generators.
|
||||
add_custom_command(
|
||||
TARGET translation_loader_test
|
||||
POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${TRANSLATION_LOADER_TEST_QM}"
|
||||
"$<TARGET_FILE_DIR:translation_loader_test>/"
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
add_executable(report_categories_test report_categories_test.cpp)
|
||||
add_executable(lag_monitor_test ${CMAKE_SOURCE_DIR}/cockatrice/src/client/lag_monitor.cpp lag_monitor_test.cpp)
|
||||
target_include_directories(lag_monitor_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
||||
add_executable(latency_tracker_test latency_tracker_test.cpp)
|
||||
add_executable(metrics_registry_test ../servatrice/src/metrics_registry.cpp metrics_registry_test.cpp)
|
||||
add_executable(
|
||||
card_update_progress_test ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_database_update/card_update_progress.cpp
|
||||
card_update_progress_test.cpp
|
||||
)
|
||||
target_include_directories(card_update_progress_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
||||
add_executable(
|
||||
loader_local_matching_test
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/cache_settings.cpp
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/card_counter_settings.cpp
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/shortcuts_settings.cpp
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp
|
||||
${VERSION_STRING_CPP}
|
||||
loader_local_matching_test.cpp
|
||||
)
|
||||
target_include_directories(loader_local_matching_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
||||
|
||||
# Guard must match the condition for src/single_instance_manager.cpp in cockatrice/CMakeLists.txt.
|
||||
if(WITH_CLIENT)
|
||||
add_executable(
|
||||
single_instance_manager_test ${CMAKE_SOURCE_DIR}/cockatrice/src/single_instance_manager.cpp
|
||||
single_instance_manager_test.cpp
|
||||
)
|
||||
target_include_directories(single_instance_manager_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
||||
target_include_directories(single_instance_manager_test PRIVATE ${CMAKE_SOURCE_DIR}/libcockatrice_utility)
|
||||
|
||||
add_test(NAME single_instance_manager_test COMMAND single_instance_manager_test)
|
||||
endif()
|
||||
|
||||
find_package(GTest)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
|
|
@ -129,93 +30,342 @@ if(NOT GTEST_FOUND)
|
|||
target_include_directories(gtest INTERFACE "$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/gtest-src/include>")
|
||||
|
||||
set(GTEST_INCLUDE_DIRS "${CMAKE_BINARY_DIR}/gtest-src/include")
|
||||
set(GTEST_BOTH_LIBRARIES gtest)
|
||||
add_dependencies(dummy_test gtest)
|
||||
add_dependencies(expression_test gtest)
|
||||
add_dependencies(clamped_arithmetic_test gtest)
|
||||
add_dependencies(test_age_formatting gtest)
|
||||
add_dependencies(password_hash_test gtest)
|
||||
add_dependencies(playmat_resolver_test gtest)
|
||||
add_dependencies(deck_hash_performance_test gtest)
|
||||
add_dependencies(server_card_counter_test gtest)
|
||||
add_dependencies(server_counter_test gtest)
|
||||
add_dependencies(server_rate_limiter_test gtest)
|
||||
add_dependencies(server_developer_role_test gtest)
|
||||
add_dependencies(server_game_join_test gtest)
|
||||
add_dependencies(warning_categories_test gtest)
|
||||
if(TARGET translation_loader_test)
|
||||
add_dependencies(translation_loader_test gtest)
|
||||
endif()
|
||||
add_dependencies(report_categories_test gtest)
|
||||
add_dependencies(lag_monitor_test gtest)
|
||||
add_dependencies(latency_tracker_test gtest)
|
||||
add_dependencies(metrics_registry_test gtest)
|
||||
add_dependencies(card_update_progress_test gtest)
|
||||
add_dependencies(loader_local_matching_test gtest)
|
||||
add_dependencies(single_instance_manager_test gtest)
|
||||
# gtest_main is not optional here. FindGTest sets GTEST_BOTH_LIBRARIES to both halves, so this
|
||||
# has to match it or the two paths disagree about what provides main(). A test source that
|
||||
# defines its own main() gets away with the omission; one that does not fails at link with
|
||||
# `undefined reference to main` pointing at crt1.o, which names neither gtest nor this line.
|
||||
set(GTEST_BOTH_LIBRARIES gtest gtest_main)
|
||||
endif()
|
||||
|
||||
# Directory-scoped on purpose: the test subdirectories below add targets that include the
|
||||
# fetched gtest headers and do not repeat this path themselves.
|
||||
include_directories(${GTEST_INCLUDE_DIRS})
|
||||
target_link_libraries(dummy_test Threads::Threads ${GTEST_BOTH_LIBRARIES})
|
||||
target_link_libraries(expression_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES})
|
||||
target_link_libraries(
|
||||
clamped_arithmetic_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(
|
||||
test_age_formatting libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(
|
||||
password_hash_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(
|
||||
playmat_resolver_test libcockatrice_deck_list libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(
|
||||
deck_hash_performance_test libcockatrice_deck_list libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(
|
||||
server_card_counter_test libcockatrice_network Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(
|
||||
server_counter_test libcockatrice_network Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(
|
||||
server_rate_limiter_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(
|
||||
server_developer_role_test libcockatrice_network libcockatrice_rng Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(
|
||||
server_game_join_test libcockatrice_network_server_remote libcockatrice_rng Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(
|
||||
warning_categories_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
if(TARGET translation_loader_test)
|
||||
|
||||
# --- Test registration ----------------------------------------------------------
|
||||
#
|
||||
# Every test target needs the same four steps: build the executable, depend on gtest when it
|
||||
# was fetched from source, link the shared test libraries, and hand the cases to CTest. The
|
||||
# `if(NOT GTEST_FOUND)` branch is the easy one to forget — omit it and the link fails with
|
||||
# undefined gtest symbols — so it lives here once instead of being repeated per test.
|
||||
#
|
||||
# This also replaces `add_test(NAME x COMMAND x)`. Naming the binary as a single CTest entry
|
||||
# hides which case failed and makes `ctest -R Suite.Case` impossible; discovery exposes each
|
||||
# TEST() as its own CTest entry instead.
|
||||
#
|
||||
# Labels: exactly one per test, because gtest_discover_tests() cannot carry more than one.
|
||||
# GoogleTestAddTests.cmake accumulates the generated script with `foreach(arg ${ARGN})`, so a
|
||||
# label list serialises as bare `LABELS ci server` in the CTestTestfile, and CTest reads only
|
||||
# the first value of a property and silently discards the rest. A label therefore classifies a
|
||||
# test into exactly one group, and the three groups cover every selection worth making:
|
||||
#
|
||||
# ci the default hermetic set, so `ctest -LE bench` runs everything but benchmarks
|
||||
# server servatrice-facing tests, so `ctest -L server` works
|
||||
# bench the slow suites, kept out of the default run
|
||||
#
|
||||
# A second label is a configure-time FATAL_ERROR rather than a line CTest quietly reads as one.
|
||||
# Anything orthogonal to a test's kind belongs in a helper flag beside the label, so it
|
||||
# composes with the group instead of competing for the single slot. NO_SANITIZE is such a flag.
|
||||
#
|
||||
# TIMEOUT is bounded per test because CTest's implicit default is 1500s: one hung socket wait
|
||||
# would otherwise stall CI for 25 minutes.
|
||||
#
|
||||
# EXTRA_LIBS is appended after the shared libraries so a caller adding a static archive (zlib,
|
||||
# lzma) keeps it last, which is where the linker needs it.
|
||||
|
||||
function(cockatrice_add_test)
|
||||
cmake_parse_arguments(
|
||||
PARSE_ARGV 0 ARG "MANUAL;NO_SANITIZE" "NAME" "SOURCES;LIBS;LABELS;DEFINITIONS;INCLUDE_DIRS;EXTRA_LIBS"
|
||||
)
|
||||
|
||||
if(NOT ARG_NAME OR NOT ARG_SOURCES)
|
||||
message(FATAL_ERROR "cockatrice_add_test requires at least NAME and SOURCES")
|
||||
endif()
|
||||
|
||||
add_executable(${ARG_NAME} ${ARG_SOURCES})
|
||||
|
||||
if(ARG_INCLUDE_DIRS)
|
||||
target_include_directories(${ARG_NAME} PRIVATE ${ARG_INCLUDE_DIRS})
|
||||
endif()
|
||||
|
||||
if(ARG_DEFINITIONS)
|
||||
target_compile_definitions(${ARG_NAME} PRIVATE ${ARG_DEFINITIONS})
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
translation_loader_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
${ARG_NAME} ${ARG_LIBS} Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} ${ARG_EXTRA_LIBS}
|
||||
)
|
||||
|
||||
# Only needed when gtest was downloaded and add_subdirectory'd above. An imported gtest
|
||||
# target already carries its own dependency, so this must stay conditional.
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(${ARG_NAME} gtest gtest_main)
|
||||
endif()
|
||||
|
||||
# MANUAL targets are built but deliberately kept out of ctest: benchmarks that need a card
|
||||
# database, hit the network, or measure RAM. Leaving them unregistered keeps CI hermetic
|
||||
# and fast. They still link gtest so they can use TEST() without a second code path.
|
||||
if(ARG_MANUAL)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# NO_SANITIZE marks a target the sanitizer leg cannot run. Excluded by not handing its cases
|
||||
# to CTest at all, rather than by a label, so the exclusion cannot rot into a line CTest
|
||||
# quietly reads as something else. The target still builds, and still runs on the other legs.
|
||||
if(ARG_NO_SANITIZE AND COCKATRICE_SANITIZERS)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# One label only, per the note above. Rejecting a second loudly beats emitting a
|
||||
# CTestTestfile line that CTest will quietly read as one label.
|
||||
list(LENGTH ARG_LABELS _label_count)
|
||||
if(_label_count GREATER 1)
|
||||
message(FATAL_ERROR "${ARG_NAME}: pass at most one label, got '${ARG_LABELS}'")
|
||||
endif()
|
||||
if(NOT ARG_LABELS)
|
||||
set(ARG_LABELS ci)
|
||||
endif()
|
||||
|
||||
# GoogleTestAddTests defaults DISCOVERY_TIMEOUT to 5s, which is ample for a plain debug
|
||||
# binary (--gtest_list_tests costs 0.04s) but not for an instrumented one (~2.8s cold, and
|
||||
# discovery runs several binaries concurrently behind a -j build). Left at the default, a
|
||||
# sanitizer build fails at the *link* step with a bare "Process terminated due to timeout"
|
||||
# and no hint that discovery, not the test, is what is slow.
|
||||
if(COCKATRICE_SANITIZERS)
|
||||
set(_discovery_timeout 300)
|
||||
else()
|
||||
set(_discovery_timeout 60)
|
||||
endif()
|
||||
|
||||
gtest_discover_tests(
|
||||
${ARG_NAME} DISCOVERY_MODE POST_BUILD DISCOVERY_TIMEOUT ${_discovery_timeout}
|
||||
PROPERTIES LABELS "${ARG_LABELS}"
|
||||
TIMEOUT 60
|
||||
)
|
||||
endfunction()
|
||||
|
||||
# Same wiring for targets that are built but deliberately kept out of ctest: benchmarks that
|
||||
# need a card database, hit the network, or measure RAM. A separate name rather than a MANUAL
|
||||
# flag spelled out at each call site, so "this is not in the CI suite" is visible in the CMake
|
||||
# file. Note the unquoted expansions: an unset multi-value keyword expands to zero arguments,
|
||||
# which is what lets this forward a caller's keywords verbatim without tripping the FATAL_ERROR
|
||||
# guard above on an empty list.
|
||||
function(cockatrice_add_manual_test)
|
||||
cmake_parse_arguments(PARSE_ARGV 0 ARG "" "NAME" "SOURCES;LIBS;DEFINITIONS;INCLUDE_DIRS;EXTRA_LIBS")
|
||||
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
${ARG_NAME}
|
||||
SOURCES
|
||||
${ARG_SOURCES}
|
||||
LIBS
|
||||
${ARG_LIBS}
|
||||
DEFINITIONS
|
||||
${ARG_DEFINITIONS}
|
||||
INCLUDE_DIRS
|
||||
${ARG_INCLUDE_DIRS}
|
||||
EXTRA_LIBS
|
||||
${ARG_EXTRA_LIBS}
|
||||
MANUAL
|
||||
)
|
||||
endfunction()
|
||||
|
||||
# --- Unit tests ------------------------------------------------------------------
|
||||
|
||||
cockatrice_add_test(NAME expression_test SOURCES expression_test.cpp LIBS libcockatrice_utility)
|
||||
cockatrice_add_test(NAME clamped_arithmetic_test SOURCES clamped_arithmetic_test.cpp LIBS libcockatrice_utility)
|
||||
cockatrice_add_test(NAME test_age_formatting SOURCES test_age_formatting.cpp LIBS libcockatrice_utility)
|
||||
cockatrice_add_test(NAME password_hash_test SOURCES password_hash_test.cpp LIBS libcockatrice_utility)
|
||||
cockatrice_add_test(NAME report_categories_test SOURCES report_categories_test.cpp LIBS libcockatrice_utility)
|
||||
cockatrice_add_test(NAME warning_categories_test SOURCES warning_categories_test.cpp LIBS libcockatrice_utility)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
playmat_resolver_test
|
||||
SOURCES
|
||||
playmat_resolver_test.cpp
|
||||
LIBS
|
||||
libcockatrice_deck_list
|
||||
libcockatrice_utility
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
server_rate_limiter_test
|
||||
SOURCES
|
||||
server_rate_limiter_test.cpp
|
||||
LIBS
|
||||
libcockatrice_utility
|
||||
LABELS
|
||||
server
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
server_card_counter_test
|
||||
SOURCES
|
||||
server_card_counter_test.cpp
|
||||
LIBS
|
||||
libcockatrice_network
|
||||
LABELS
|
||||
server
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
server_counter_test
|
||||
SOURCES
|
||||
server_counter_test.cpp
|
||||
LIBS
|
||||
libcockatrice_network
|
||||
LABELS
|
||||
server
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
server_developer_role_test
|
||||
NO_SANITIZE
|
||||
SOURCES
|
||||
server_developer_role_test.cpp
|
||||
LIBS
|
||||
libcockatrice_network
|
||||
libcockatrice_rng
|
||||
LABELS
|
||||
server
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
server_moderator_role_test
|
||||
NO_SANITIZE
|
||||
SOURCES
|
||||
server_moderator_role_test.cpp
|
||||
LIBS
|
||||
libcockatrice_network
|
||||
libcockatrice_rng
|
||||
LABELS
|
||||
server
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
server_game_join_test
|
||||
SOURCES
|
||||
server_game_join_test.cpp
|
||||
LIBS
|
||||
libcockatrice_network_server_remote
|
||||
libcockatrice_rng
|
||||
LABELS
|
||||
server
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
latency_tracker_test
|
||||
SOURCES
|
||||
latency_tracker_test.cpp
|
||||
LIBS
|
||||
libcockatrice_network
|
||||
LABELS
|
||||
server
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
lag_monitor_test
|
||||
SOURCES
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/lag_monitor.cpp
|
||||
lag_monitor_test.cpp
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
metrics_registry_test
|
||||
SOURCES
|
||||
${CMAKE_SOURCE_DIR}/servatrice/src/metrics_registry.cpp
|
||||
metrics_registry_test.cpp
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/servatrice/src
|
||||
LABELS
|
||||
server
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
card_update_progress_test
|
||||
SOURCES
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_database_update/card_update_progress.cpp
|
||||
card_update_progress_test.cpp
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
loader_local_matching_test
|
||||
SOURCES
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/cache_settings.cpp
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/card_counter_settings.cpp
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/shortcuts_settings.cpp
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp
|
||||
${VERSION_STRING_CPP}
|
||||
loader_local_matching_test.cpp
|
||||
LIBS
|
||||
libcockatrice_settings
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
deck_hash_performance_test
|
||||
SOURCES
|
||||
deck_hash_performance_test.cpp
|
||||
LIBS
|
||||
libcockatrice_deck_list
|
||||
libcockatrice_utility
|
||||
LABELS
|
||||
bench
|
||||
)
|
||||
|
||||
# The translation loader test compiles a .qm fixture, which needs lrelease from Qt Linguist.
|
||||
if(TARGET Qt6::lrelease)
|
||||
set(TRANSLATION_LOADER_TEST_QM "${CMAKE_CURRENT_BINARY_DIR}/translation_loader_test_de.qm")
|
||||
add_custom_command(
|
||||
OUTPUT "${TRANSLATION_LOADER_TEST_QM}"
|
||||
COMMAND Qt6::lrelease "${CMAKE_CURRENT_SOURCE_DIR}/translation_loader_test.ts" -qm "${TRANSLATION_LOADER_TEST_QM}"
|
||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/translation_loader_test.ts"
|
||||
COMMENT "Compiling translation_loader_test.ts"
|
||||
)
|
||||
add_custom_target(translation_loader_test_qm DEPENDS "${TRANSLATION_LOADER_TEST_QM}")
|
||||
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
translation_loader_test
|
||||
SOURCES
|
||||
translation_loader_test.cpp
|
||||
LIBS
|
||||
libcockatrice_utility
|
||||
DEFINITIONS
|
||||
TRANSLATION_LOADER_TEST_DIR="${CMAKE_CURRENT_BINARY_DIR}"
|
||||
)
|
||||
add_dependencies(translation_loader_test translation_loader_test_qm)
|
||||
|
||||
# A multi-config generator (MSBuild, Xcode) puts the executable in a per-config subdirectory while
|
||||
# the .qm stays in the current binary directory, so copy the fixture next to the binary: that is the
|
||||
# layout ABuildTreeFindsTheTranslationsNextToTheExecutable is about, and the only one it finds on
|
||||
# those generators.
|
||||
add_custom_command(
|
||||
TARGET translation_loader_test
|
||||
POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${TRANSLATION_LOADER_TEST_QM}"
|
||||
"$<TARGET_FILE_DIR:translation_loader_test>/"
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
target_link_libraries(
|
||||
report_categories_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(lag_monitor_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES})
|
||||
target_link_libraries(
|
||||
latency_tracker_test libcockatrice_network Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
target_include_directories(metrics_registry_test PRIVATE ${CMAKE_SOURCE_DIR}/servatrice/src)
|
||||
target_link_libraries(metrics_registry_test ${TEST_QT_MODULES} Threads::Threads ${GTEST_BOTH_LIBRARIES})
|
||||
target_link_libraries(card_update_progress_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES})
|
||||
target_link_libraries(
|
||||
loader_local_matching_test libcockatrice_settings Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
|
||||
# Guard must match the condition for src/single_instance_manager.cpp in cockatrice/CMakeLists.txt.
|
||||
if(WITH_CLIENT)
|
||||
target_link_libraries(single_instance_manager_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES})
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
single_instance_manager_test
|
||||
SOURCES
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/single_instance_manager.cpp
|
||||
single_instance_manager_test.cpp
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
${CMAKE_SOURCE_DIR}/libcockatrice_utility
|
||||
)
|
||||
endif()
|
||||
|
||||
add_subdirectory(card_zone_algorithms)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,4 @@
|
|||
add_executable(card_zone_algorithms_test card_zone_algorithms_test.cpp)
|
||||
|
||||
target_include_directories(card_zone_algorithms_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src/game/zones)
|
||||
|
||||
target_link_libraries(
|
||||
card_zone_algorithms_test
|
||||
PRIVATE Threads::Threads
|
||||
PRIVATE ${GTEST_BOTH_LIBRARIES}
|
||||
cockatrice_add_test(
|
||||
NAME card_zone_algorithms_test SOURCES card_zone_algorithms_test.cpp INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/game/zones
|
||||
)
|
||||
|
||||
add_test(NAME card_zone_algorithms_test COMMAND card_zone_algorithms_test)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(card_zone_algorithms_test gtest)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -9,61 +9,46 @@ add_definitions("-DCARDDB_DATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}/data/\"")
|
|||
# ------------------------
|
||||
# Card Database Test
|
||||
# ------------------------
|
||||
add_executable(carddatabase_test ${MOCKS_SOURCES} ${VERSION_STRING_CPP} carddatabase_test.cpp mocks.cpp)
|
||||
|
||||
target_link_libraries(
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
carddatabase_test
|
||||
PRIVATE libcockatrice_card
|
||||
PRIVATE Threads::Threads
|
||||
PRIVATE ${GTEST_BOTH_LIBRARIES}
|
||||
PRIVATE ${TEST_QT_MODULES}
|
||||
SOURCES
|
||||
${VERSION_STRING_CPP}
|
||||
carddatabase_test.cpp
|
||||
mocks.cpp
|
||||
LIBS
|
||||
libcockatrice_card
|
||||
)
|
||||
|
||||
add_test(NAME carddatabase_test COMMAND carddatabase_test)
|
||||
|
||||
# ------------------------
|
||||
# Load Benchmark (manual, not run in CI)
|
||||
# ------------------------
|
||||
add_executable(load_benchmark ${MOCKS_SOURCES} ${VERSION_STRING_CPP} load_benchmark.cpp mocks.cpp)
|
||||
|
||||
target_link_libraries(
|
||||
load_benchmark
|
||||
PRIVATE libcockatrice_card
|
||||
PRIVATE libcockatrice_models
|
||||
PRIVATE Threads::Threads
|
||||
PRIVATE ${GTEST_BOTH_LIBRARIES}
|
||||
PRIVATE ${TEST_QT_MODULES}
|
||||
)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(load_benchmark gtest)
|
||||
endif()
|
||||
|
||||
# ------------------------
|
||||
# Filter String Test
|
||||
# (guard must match the condition for libcockatrice_filters in the root CMakeLists.txt)
|
||||
# ------------------------
|
||||
if(WITH_ORACLE OR WITH_CLIENT)
|
||||
add_executable(filter_string_test ${MOCKS_SOURCES} ${VERSION_STRING_CPP} filter_string_test.cpp mocks.cpp)
|
||||
|
||||
target_link_libraries(
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
filter_string_test
|
||||
PRIVATE libcockatrice_filters
|
||||
PRIVATE Threads::Threads
|
||||
PRIVATE ${GTEST_BOTH_LIBRARIES}
|
||||
PRIVATE ${TEST_QT_MODULES}
|
||||
SOURCES
|
||||
${VERSION_STRING_CPP}
|
||||
filter_string_test.cpp
|
||||
mocks.cpp
|
||||
LIBS
|
||||
libcockatrice_filters
|
||||
)
|
||||
|
||||
add_test(NAME filter_string_test COMMAND filter_string_test)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(filter_string_test gtest)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# ------------------------
|
||||
# Dependencies on gtest
|
||||
# Load Benchmark (manual, not run in CI)
|
||||
# ------------------------
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(carddatabase_test gtest)
|
||||
endif()
|
||||
cockatrice_add_manual_test(
|
||||
NAME
|
||||
load_benchmark
|
||||
SOURCES
|
||||
${VERSION_STRING_CPP}
|
||||
load_benchmark.cpp
|
||||
mocks.cpp
|
||||
LIBS
|
||||
libcockatrice_card
|
||||
libcockatrice_models
|
||||
MANUAL
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,33 +1,23 @@
|
|||
add_executable(deck_list_model_custom_zones_test ${VERSION_STRING_CPP} deck_list_model_custom_zones_test.cpp)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(deck_list_model_custom_zones_test gtest)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
deck_list_model_custom_zones_test
|
||||
SOURCES
|
||||
${VERSION_STRING_CPP}
|
||||
deck_list_model_custom_zones_test.cpp
|
||||
LIBS
|
||||
libcockatrice_models
|
||||
libcockatrice_card
|
||||
libcockatrice_deck_list
|
||||
Threads::Threads
|
||||
${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
)
|
||||
add_test(NAME deck_list_model_custom_zones_test COMMAND deck_list_model_custom_zones_test)
|
||||
|
||||
add_executable(deck_list_model_zone_integration_test ${VERSION_STRING_CPP} deck_list_model_zone_integration_test.cpp)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(deck_list_model_zone_integration_test gtest)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
deck_list_model_zone_integration_test
|
||||
SOURCES
|
||||
${VERSION_STRING_CPP}
|
||||
deck_list_model_zone_integration_test.cpp
|
||||
LIBS
|
||||
libcockatrice_models
|
||||
libcockatrice_card
|
||||
libcockatrice_deck_list
|
||||
Threads::Threads
|
||||
${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
)
|
||||
add_test(NAME deck_list_model_zone_integration_test COMMAND deck_list_model_zone_integration_test)
|
||||
|
|
|
|||
|
|
@ -1,10 +1 @@
|
|||
add_executable(deck_list_zones_test deck_list_zones_test.cpp)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(deck_list_zones_test gtest)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
deck_list_zones_test libcockatrice_deck_list Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
add_test(NAME deck_list_zones_test COMMAND deck_list_zones_test)
|
||||
cockatrice_add_test(NAME deck_list_zones_test SOURCES deck_list_zones_test.cpp LIBS libcockatrice_deck_list)
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
#include "gtest/gtest.h"
|
||||
#include <string>
|
||||
|
||||
namespace
|
||||
{
|
||||
class FooTest : public ::testing::Test
|
||||
{
|
||||
};
|
||||
|
||||
TEST(DummyTest, Works)
|
||||
{
|
||||
ASSERT_EQ(1, 1) << "One is not equal to one";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
add_definitions("-DCARDDB_DATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}/data/\"")
|
||||
add_executable(loading_from_clipboard_test ${VERSION_STRING_CPP} clipboard_testing.cpp loading_from_clipboard_test.cpp)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(loading_from_clipboard_test gtest)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
loading_from_clipboard_test libcockatrice_deck_list libcockatrice_card Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
loading_from_clipboard_test
|
||||
SOURCES
|
||||
${VERSION_STRING_CPP}
|
||||
clipboard_testing.cpp
|
||||
loading_from_clipboard_test.cpp
|
||||
LIBS
|
||||
libcockatrice_deck_list
|
||||
libcockatrice_card
|
||||
)
|
||||
add_test(NAME loading_from_clipboard_test COMMAND loading_from_clipboard_test)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,11 @@
|
|||
add_executable(reverse_card_move_test reverse_card_move_test.cpp)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(reverse_card_move_test gtest)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
reverse_card_move_test
|
||||
PRIVATE libcockatrice_network_server_remote
|
||||
PRIVATE libcockatrice_rng
|
||||
PRIVATE Threads::Threads
|
||||
PRIVATE ${GTEST_BOTH_LIBRARIES}
|
||||
PRIVATE ${TEST_QT_MODULES}
|
||||
SOURCES
|
||||
reverse_card_move_test.cpp
|
||||
LIBS
|
||||
libcockatrice_network_server_remote
|
||||
libcockatrice_rng
|
||||
LABELS
|
||||
server
|
||||
)
|
||||
|
||||
add_test(NAME reverse_card_move_test COMMAND reverse_card_move_test)
|
||||
|
|
|
|||
|
|
@ -1,37 +1,32 @@
|
|||
add_executable(parse_cipt_test ../../oracle/src/parsehelpers.cpp parse_cipt_test.cpp)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(parse_cipt_test gtest)
|
||||
endif()
|
||||
|
||||
target_link_libraries(parse_cipt_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES})
|
||||
|
||||
add_test(NAME parse_cipt_test COMMAND parse_cipt_test)
|
||||
cockatrice_add_test(NAME parse_cipt_test SOURCES ../../oracle/src/parsehelpers.cpp parse_cipt_test.cpp)
|
||||
|
||||
# Oracle importer unit tests
|
||||
add_executable(
|
||||
oracle_importer_test ${VERSION_STRING_CPP} ../../oracle/src/oracleimporter.cpp ../../oracle/src/parsehelpers.cpp
|
||||
../../oracle/src/raw_json_scanner.cpp oracle_importer_test.cpp
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
oracle_importer_test
|
||||
SOURCES
|
||||
${VERSION_STRING_CPP}
|
||||
../../oracle/src/oracleimporter.cpp
|
||||
../../oracle/src/parsehelpers.cpp
|
||||
../../oracle/src/raw_json_scanner.cpp
|
||||
oracle_importer_test.cpp
|
||||
LIBS
|
||||
libcockatrice_card
|
||||
libcockatrice_interfaces
|
||||
)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(oracle_importer_test gtest)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
oracle_importer_test libcockatrice_card libcockatrice_interfaces Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
)
|
||||
|
||||
add_test(NAME oracle_importer_test COMMAND oracle_importer_test)
|
||||
|
||||
# Oracle importer benchmark tests (manual, not run in CI, incl. RAM benchmark)
|
||||
# Optional compression libs, mirrored from oracle/CMakeLists.txt, so the benchmark
|
||||
# can download and decompress whatever AllPrintings format the default URL selects.
|
||||
# add_definitions() is directory-scoped and order-independent, so HAS_ZLIB/HAS_LZMA also
|
||||
# reach the two unit tests above. That is harmless: those macros gate code only in
|
||||
# oracle/src/pages.cpp and oracle/src/pagetemplates.cpp, which no test target compiles.
|
||||
find_package(ZLIB)
|
||||
if(ZLIB_FOUND)
|
||||
add_definitions("-DHAS_ZLIB")
|
||||
set(_ORACLE_BENCH_EXTRA_SOURCES ../../oracle/src/zip/unzip.cpp ../../oracle/src/zip/zipglobal.cpp)
|
||||
cockatrice_disable_sanitizers_for(${_ORACLE_BENCH_EXTRA_SOURCES})
|
||||
cockatrice_disable_coverage_for(${_ORACLE_BENCH_EXTRA_SOURCES})
|
||||
set(_ORACLE_BENCH_EXTRA_LIBRARIES ${ZLIB_LIBRARIES})
|
||||
include_directories(${ZLIB_INCLUDE_DIRS})
|
||||
else()
|
||||
|
|
@ -42,28 +37,28 @@ find_package(LibLZMA)
|
|||
if(LIBLZMA_FOUND)
|
||||
add_definitions("-DHAS_LZMA")
|
||||
list(APPEND _ORACLE_BENCH_EXTRA_SOURCES ../../oracle/src/lzma/decompress.cpp)
|
||||
cockatrice_disable_sanitizers_for(../../oracle/src/lzma/decompress.cpp)
|
||||
cockatrice_disable_coverage_for(../../oracle/src/lzma/decompress.cpp)
|
||||
list(APPEND _ORACLE_BENCH_EXTRA_LIBRARIES ${LIBLZMA_LIBRARIES})
|
||||
include_directories(${LIBLZMA_INCLUDE_DIRS})
|
||||
else()
|
||||
message(STATUS "Oracle tests: LibLZMA not found; xz download benchmark disabled")
|
||||
endif()
|
||||
|
||||
add_executable(
|
||||
oracle_importer_benchmark_test
|
||||
${VERSION_STRING_CPP} ../../oracle/src/oracleimporter.cpp ../../oracle/src/parsehelpers.cpp
|
||||
../../oracle/src/raw_json_scanner.cpp oracle_importer_benchmark_test.cpp ${_ORACLE_BENCH_EXTRA_SOURCES}
|
||||
)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(oracle_importer_benchmark_test gtest)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
cockatrice_add_manual_test(
|
||||
NAME
|
||||
oracle_importer_benchmark_test
|
||||
SOURCES
|
||||
${VERSION_STRING_CPP}
|
||||
../../oracle/src/oracleimporter.cpp
|
||||
../../oracle/src/parsehelpers.cpp
|
||||
../../oracle/src/raw_json_scanner.cpp
|
||||
oracle_importer_benchmark_test.cpp
|
||||
${_ORACLE_BENCH_EXTRA_SOURCES}
|
||||
LIBS
|
||||
libcockatrice_card
|
||||
libcockatrice_interfaces
|
||||
Threads::Threads
|
||||
${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
EXTRA_LIBS
|
||||
${_ORACLE_BENCH_EXTRA_LIBRARIES}
|
||||
MANUAL
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
# Manual benchmark hitting the real card image hosts. Not registered with
|
||||
# add_test(): it requires a cards.xml, touches the network for minutes at a
|
||||
# time, and needs network access. Build it explicitly and run by hand.
|
||||
add_executable(
|
||||
# Manual benchmark hitting the real card image hosts. Not registered with CTest: it requires a
|
||||
# cards.xml, touches the network for minutes at a time, and needs network access. Build it
|
||||
# explicitly and run by hand.
|
||||
cockatrice_add_manual_test(
|
||||
NAME
|
||||
picture_loader_benchmark_test
|
||||
SOURCES
|
||||
${VERSION_STRING_CPP}
|
||||
../../cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
|
||||
../../cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp
|
||||
|
|
@ -11,16 +13,12 @@ add_executable(
|
|||
../../cockatrice/src/client/settings/cache_settings.h
|
||||
picture_loader_benchmark.cpp
|
||||
settings_cache_mock.cpp
|
||||
)
|
||||
|
||||
target_include_directories(picture_loader_benchmark_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
||||
|
||||
target_link_libraries(
|
||||
picture_loader_benchmark_test
|
||||
LIBS
|
||||
libcockatrice_card
|
||||
libcockatrice_settings
|
||||
libcockatrice_interfaces
|
||||
libcockatrice_utility
|
||||
Threads::Threads
|
||||
${TEST_QT_MODULES}
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
MANUAL
|
||||
)
|
||||
|
|
|
|||
156
tests/server_moderator_role_test.cpp
Normal file
156
tests/server_moderator_role_test.cpp
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
/** @file server_moderator_role_test.cpp
|
||||
* @brief Tests for the moderator staff role authorization and dispatch.
|
||||
* @ingroup Tests
|
||||
*/
|
||||
|
||||
#include "libcockatrice/protocol/pb/response.pb.h"
|
||||
#include "libcockatrice/protocol/pb/server_message.pb.h"
|
||||
|
||||
#include <QString>
|
||||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/network/server/remote/server.h>
|
||||
#include <libcockatrice/network/server/remote/server_protocolhandler.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_reopen.pb.h>
|
||||
#include <libcockatrice/protocol/pb/commands.pb.h>
|
||||
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
#include <libcockatrice/rng/rng_abstract.h>
|
||||
#include <stdint.h>
|
||||
#include <string>
|
||||
|
||||
// The server_remote library references the global RNG, which is normally
|
||||
// defined by the servatrice/client executable main(). Provide a stub so the
|
||||
// unit test can link against it.
|
||||
RNG_Abstract *rng = nullptr;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class TestModeratorHandler : public Server_ProtocolHandler
|
||||
{
|
||||
public:
|
||||
explicit TestModeratorHandler(Server *_server) : Server_ProtocolHandler(_server, nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
QString getAddress() const override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
QString getConnectionType() const override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
// Buffer the last response code sent to the client so tests can assert on
|
||||
// the outcome of processCommandContainer().
|
||||
Response::ResponseCode lastResponseCode = Response::RespNothing;
|
||||
int dispatchCount = 0;
|
||||
|
||||
protected:
|
||||
void transmitProtocolItem(const ServerMessage &item) override
|
||||
{
|
||||
if (item.message_type() == ServerMessage::RESPONSE) {
|
||||
lastResponseCode = item.response().response_code();
|
||||
}
|
||||
}
|
||||
|
||||
Response::ResponseCode
|
||||
processExtendedModeratorCommand(int cmdType, const ModeratorCommand &, ResponseContainer &) override
|
||||
{
|
||||
++dispatchCount;
|
||||
// Fail closed for anything not explicitly handled.
|
||||
if (cmdType != ModeratorCommand::REPORT_REOPEN) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
return Response::RespOk;
|
||||
}
|
||||
};
|
||||
|
||||
class ModeratorRoleTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
Server server;
|
||||
TestModeratorHandler handler{&server};
|
||||
|
||||
void setUserLevel(uint32_t level)
|
||||
{
|
||||
ServerInfo_User user;
|
||||
user.set_user_level(level);
|
||||
handler.setUserInfo(user);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ModeratorRoleTest, RejectsWhenNotLoggedIn)
|
||||
{
|
||||
CommandContainer cont;
|
||||
cont.add_moderator_command();
|
||||
handler.processCommandContainer(cont);
|
||||
EXPECT_EQ(handler.lastResponseCode, Response::RespLoginNeeded);
|
||||
EXPECT_EQ(handler.dispatchCount, 0);
|
||||
}
|
||||
|
||||
TEST_F(ModeratorRoleTest, RejectsPlainUser)
|
||||
{
|
||||
setUserLevel(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered);
|
||||
|
||||
CommandContainer cont;
|
||||
cont.add_moderator_command();
|
||||
handler.processCommandContainer(cont);
|
||||
EXPECT_EQ(handler.lastResponseCode, Response::RespLoginNeeded);
|
||||
EXPECT_EQ(handler.dispatchCount, 0);
|
||||
}
|
||||
|
||||
TEST_F(ModeratorRoleTest, DispatchesToModeratorCommandForModerator)
|
||||
{
|
||||
setUserLevel(ServerInfo_User::IsModerator);
|
||||
|
||||
CommandContainer cont;
|
||||
ModeratorCommand *cmd = cont.add_moderator_command();
|
||||
cmd->MutableExtension(Command_ReportReopen::ext);
|
||||
handler.processCommandContainer(cont);
|
||||
EXPECT_EQ(handler.lastResponseCode, Response::RespOk);
|
||||
EXPECT_EQ(handler.dispatchCount, 1);
|
||||
}
|
||||
|
||||
TEST_F(ModeratorRoleTest, DispatchesToModeratorCommandForAdmin)
|
||||
{
|
||||
setUserLevel(ServerInfo_User::IsModerator | ServerInfo_User::IsAdmin);
|
||||
|
||||
CommandContainer cont;
|
||||
ModeratorCommand *cmd = cont.add_moderator_command();
|
||||
cmd->MutableExtension(Command_ReportReopen::ext);
|
||||
handler.processCommandContainer(cont);
|
||||
EXPECT_EQ(handler.lastResponseCode, Response::RespOk);
|
||||
EXPECT_EQ(handler.dispatchCount, 1);
|
||||
}
|
||||
|
||||
TEST_F(ModeratorRoleTest, RejectsDeveloperThatIsNotModerator)
|
||||
{
|
||||
setUserLevel(ServerInfo_User::IsDeveloper);
|
||||
|
||||
CommandContainer cont;
|
||||
cont.add_moderator_command();
|
||||
handler.processCommandContainer(cont);
|
||||
EXPECT_EQ(handler.lastResponseCode, Response::RespLoginNeeded);
|
||||
EXPECT_EQ(handler.dispatchCount, 0);
|
||||
}
|
||||
|
||||
TEST_F(ModeratorRoleTest, FailClosedForUnknownModeratorCommand)
|
||||
{
|
||||
setUserLevel(ServerInfo_User::IsModerator);
|
||||
|
||||
CommandContainer cont;
|
||||
cont.add_moderator_command(); // no extension set -> getPbExtension() returns -1
|
||||
handler.processCommandContainer(cont);
|
||||
EXPECT_EQ(handler.lastResponseCode, Response::RespFunctionNotAllowed);
|
||||
EXPECT_EQ(handler.dispatchCount, 1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
|
@ -1,26 +1,14 @@
|
|||
add_executable(settings_manager_test settings_manager_test.cpp)
|
||||
add_executable(settings_defaults_test settings_defaults_test.cpp)
|
||||
add_executable(settings_migration_test settings_migration_test.cpp)
|
||||
cockatrice_add_test(NAME settings_manager_test SOURCES settings_manager_test.cpp LIBS libcockatrice_settings)
|
||||
|
||||
target_link_libraries(
|
||||
settings_manager_test libcockatrice_settings Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
settings_defaults_test
|
||||
SOURCES
|
||||
settings_defaults_test.cpp
|
||||
LIBS
|
||||
libcockatrice_settings
|
||||
DEFINITIONS
|
||||
SETTINGS_UNIT_TEST
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
settings_defaults_test libcockatrice_settings Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
target_compile_definitions(settings_defaults_test PRIVATE SETTINGS_UNIT_TEST)
|
||||
|
||||
target_link_libraries(
|
||||
settings_migration_test libcockatrice_settings Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
|
||||
add_test(NAME settings_manager_test COMMAND settings_manager_test)
|
||||
add_test(NAME settings_defaults_test COMMAND settings_defaults_test)
|
||||
add_test(NAME settings_migration_test COMMAND settings_migration_test)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(settings_manager_test gtest)
|
||||
add_dependencies(settings_defaults_test gtest)
|
||||
add_dependencies(settings_migration_test gtest)
|
||||
endif()
|
||||
cockatrice_add_test(NAME settings_migration_test SOURCES settings_migration_test.cpp LIBS libcockatrice_settings)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "libcockatrice/card/card_localization.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QDir>
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
#include <libcockatrice/settings/game_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/settings/network_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <libcockatrice/settings/sound_settings.h>
|
||||
#include <libcockatrice/settings/tabs_settings.h>
|
||||
|
|
@ -695,6 +697,55 @@ TEST_F(SettingsDefaultsTest, VisualDeckStorage_ShowUploadTime_SetAndGet)
|
|||
ASSERT_EQ(s.getVisualDeckStorageShowUploadTime(), false);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Paths_AppDirWarningAcknowledged_Default)
|
||||
{
|
||||
PathsSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getAppDirWarningAcknowledged(), false);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Paths_AppDirWarningAcknowledged_SetAndGet)
|
||||
{
|
||||
PathsSettings s(settingsPath, nullptr);
|
||||
s.setAppDirWarningAcknowledged(true);
|
||||
ASSERT_EQ(s.getAppDirWarningAcknowledged(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Paths_PathsInsideDir_DetectsNestedAndIgnoresSiblings)
|
||||
{
|
||||
PathsSettings s(settingsPath, nullptr);
|
||||
const QString base = QDir::cleanPath(settingsPath);
|
||||
|
||||
s.setDeckPath(base + "/decks/");
|
||||
s.setPicsPath(base + "/pics");
|
||||
s.setReplaysPath(base + "2"); // sibling sharing the name prefix, not nested
|
||||
|
||||
const QStringList offenders = s.pathsInsideDir(base);
|
||||
ASSERT_EQ(offenders.size(), 2);
|
||||
ASSERT_TRUE(offenders.contains(base + "/decks"));
|
||||
ASSERT_TRUE(offenders.contains(base + "/pics"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Paths_PathsInsideDir_EmptyWhenNothingMatches)
|
||||
{
|
||||
PathsSettings s(settingsPath, nullptr);
|
||||
s.setDeckPath("/somewhere/else/decks");
|
||||
ASSERT_TRUE(s.pathsInsideDir(QDir::cleanPath(settingsPath)).isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Paths_PathsInsideDir_MatchesExactBaseAndNormalizesDots)
|
||||
{
|
||||
PathsSettings s(settingsPath, nullptr);
|
||||
const QString base = QDir::cleanPath(settingsPath);
|
||||
|
||||
s.setCustomPicsPath(base); // exactly the base directory
|
||||
s.setThemesPath(base + "/pics/../decks"); // needs cleanPath normalization
|
||||
|
||||
const QStringList offenders = s.pathsInsideDir(base);
|
||||
ASSERT_EQ(offenders.size(), 2);
|
||||
ASSERT_TRUE(offenders.contains(base));
|
||||
ASSERT_TRUE(offenders.contains(base + "/decks"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
add_executable(
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
theme_migration_test
|
||||
SOURCES
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_manager.cpp
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_config.cpp
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/pixel_map_generator.cpp
|
||||
|
|
@ -9,22 +11,11 @@ add_executable(
|
|||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp
|
||||
${VERSION_STRING_CPP}
|
||||
theme_migration_test.cpp
|
||||
)
|
||||
target_include_directories(theme_migration_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
||||
|
||||
target_link_libraries(
|
||||
theme_migration_test
|
||||
LIBS
|
||||
libcockatrice_settings
|
||||
libcockatrice_network
|
||||
libcockatrice_network_server_remote
|
||||
libcockatrice_protocol
|
||||
Threads::Threads
|
||||
${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
)
|
||||
|
||||
add_test(NAME theme_migration_test COMMAND theme_migration_test)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(theme_migration_test gtest)
|
||||
endif()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue