Skip to content
Merged

Ci #9

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/run-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/bin/sh
#
# What CI runs once a build is standing. Called with CONFIGURATION set to
# the make flags that produced the binary, so this knows which one to run.
#
# Only a default build can be tested here. Every mode calls vInit() before
# it does anything, and notcurses will not open without a terminal - under
# Actions there is none, and it aborts. The default backend is plain ANSI
# on a file descriptor and does not care, so it runs headless, and the
# game logic it exercises is the same either way: the two backends differ
# only in how the screen is drawn.
#
# Nothing here reads the keyboard on purpose, but --test polls for a key
# to see whether it has been told to stop, and treats end-of-input as
# "stop". /dev/zero never ends, so it keeps going until the timeout.

set -eu

case "${CONFIGURATION:-}" in
*debug=1*) avanor=./avanor-d ;;
*) avanor=./avanor ;;
esac

# The soak below is meant to be cut off - it has no end of its own. Only
# the timeout killing it, or a clean exit, counts as passing; anything
# else is the crash we are looking for.
SOAK_SECONDS=60

# macOS ships no timeout(1) - it is GNU coreutils, which Homebrew installs
# with every binary prefixed, so there it is gtimeout.
if command -v timeout >/dev/null 2>&1; then
timeout_cmd=timeout
elif command -v gtimeout >/dev/null 2>&1; then
timeout_cmd=gtimeout
else
echo "neither timeout nor gtimeout found - install GNU coreutils" >&2
exit 1
fi

# Where each run's stderr is caught so it can be looked at afterwards.
complaints=$(mktemp)
trap 'rm -f "$complaints"' EXIT

# Content faults - a Lua file naming something nothing declares, a
# handler that will not run - are reported on stderr and otherwise let
# the game carry on, because a player would rather have a world with one
# thing wrong in it than no world at all. CI has no such excuse: a run
# that says anything about the world has found a bug, so it fails here.
run() {
echo "==> $*"
"$@" </dev/zero >/dev/null 2>"$complaints" || {
status=$?
cat "$complaints" >&2
return $status
}

if [ -s "$complaints" ]; then
echo "the engine complained about the content:" >&2
cat "$complaints" >&2
return 1
fi
}

# One location written and read back.
run "$avanor" --test-cereal

# A world saved in one process and restored in another.
run "$avanor" --test-save
run "$avanor" --test-load

# Two scripted worlds played to their end. These run the scheduler, the
# AI, combat and the whole Lua content stack for a few hundred thousand
# turns, and answer 1 if the scenario could not be set up at all.
run "$avanor" --simulate orc_attack --arg 1 -s 1
run "$avanor" --simulate mushroom_errand --arg 2 -s 1

# The world left to run itself for a while, with no hero in it.
echo "==> $avanor --test -s 4711 (${SOAK_SECONDS}s)"
status=0
"$timeout_cmd" "$SOAK_SECONDS" "$avanor" --test -s 4711 </dev/zero >/dev/null \
2>"$complaints" || status=$?

# 124 is timeout(1) saying it had to kill it, which is the world still
# turning when time ran out - the outcome we want.
if [ "$status" -ne 0 ] && [ "$status" -ne 124 ]; then
cat "$complaints" >&2
echo "soak died with exit $status" >&2
exit 1
fi

if [ -s "$complaints" ]; then
echo "the engine complained about the content:" >&2
cat "$complaints" >&2
exit 1
fi

echo "all tests passed"
115 changes: 115 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
name: C/C++ CI

on:
push:
pull_request:
branches: ["main"]
workflow_dispatch:

# A new push to a branch makes the run already going on it pointless.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
# Every way the Makefile can be asked to build, under both compilers.
#
# The default configurations deliberately do *not* install notcurses:
# the point of the default backend is that it needs nothing but fmt and
# luajit, and the only way to keep that true is to build it on a machine
# where notcurses is absent.
#
# They are also the configurations that run the tests, because they are
# the ones that run without a terminal - see the test step below.
linux:
name: linux ${{ matrix.compiler }} ${{ matrix.configuration || 'release' }}
# Pinned rather than ubuntu-latest, which is still 24.04: sol2 was not
# packaged for Ubuntu until 26.04 (as sol2-dev). Move this forward when
# ubuntu-latest catches up.
runs-on: ubuntu-26.04
strategy:
fail-fast: false
matrix:
compiler: [gcc, clang]
configuration: ["", "debug=1", "notcurses=1", "notcurses=1 debug=1"]
include:
- compiler: gcc
cxx: g++
- compiler: clang
cxx: clang++

steps:
- uses: actions/checkout@v7
- name: Update apt cache
run: sudo apt update
- name: Install dependencies
run: sudo apt install -y libargparse-dev libcereal-dev libfmt-dev libluajit-5.1-dev libzstd-dev pkg-config sol2-dev
- name: Install notcurses
run: sudo apt install -y libnotcurses-dev libnotcurses++-dev
if: contains(matrix.configuration, 'notcurses=1')
- name: Build ${{ matrix.configuration || 'release' }}
run: make -j$(nproc) CX=${{ matrix.cxx }} LD=${{ matrix.cxx }} ${{ matrix.configuration }}
- name: Check the binary starts
run: ${{ contains(matrix.configuration, 'debug=1') && './avanor-d' || './avanor' }} --version
- name: Test
if: ${{ !contains(matrix.configuration, 'notcurses=1') }}
env:
CONFIGURATION: ${{ matrix.configuration }}
run: ./.github/run-tests.sh

# Nothing here is run: the Windows console under Actions is not a
# console either backend will open, so this proves the two builds still
# compile and link, no more.
windows:
name: windows ${{ matrix.configuration || 'release' }}
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
configuration: ["", "debug=1", "notcurses=1", "notcurses=1 debug=1"]
defaults:
run:
shell: msys2 {0}
steps:
- uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
update: true
install: >
git
make
wget
mingw-w64-ucrt-x86_64-gcc
mingw-w64-ucrt-x86_64-cereal
mingw-w64-ucrt-x86_64-fmt
mingw-w64-ucrt-x86_64-luajit
mingw-w64-ucrt-x86_64-notcurses
mingw-w64-ucrt-x86_64-pkgconf
mingw-w64-ucrt-x86_64-sol2
mingw-w64-ucrt-x86_64-zstd
- uses: actions/checkout@v7
- name: Build ${{ matrix.configuration || 'release' }}
run: make -j$(nproc) ${{ matrix.configuration }}
- name: Check the binary starts
run: ${{ contains(matrix.configuration, 'debug=1') && './avanor-d.exe' || './avanor.exe' }} --version

macos:
name: macos ${{ matrix.configuration || 'release' }}
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
configuration: ["", "debug=1", "notcurses=1", "notcurses=1 debug=1"]
steps:
- uses: actions/checkout@v7
- name: Install dependencies
run: brew install argparse cereal coreutils fmt luajit notcurses pkgconf sol2 wget zstd
- name: Build ${{ matrix.configuration || 'release' }}
run: make -j$(sysctl -n hw.ncpu) ${{ matrix.configuration }}
- name: Check the binary starts
run: ${{ contains(matrix.configuration, 'debug=1') && './avanor-d' || './avanor' }} --version
- name: Test
if: ${{ !contains(matrix.configuration, 'notcurses=1') }}
env:
CONFIGURATION: ${{ matrix.configuration }}
run: ./.github/run-tests.sh
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ dmp*.txt
obj/
obj-d/

# Fetched on Windows builds by the Makefile - see ARGPARSE_HEADER
# Fetched by the Makefile - see STC_HEADER and ARGPARSE_HEADER
external/

# Generated by tools and IDEs
Expand Down
21 changes: 19 additions & 2 deletions Main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,14 @@ static bool TestRealCreature()
return false;
}

std::cout << "TestRealCreature: runtime type is " << typeid(*original).name() << std::endl;
// Through a reference rather than typeid(*original): dereferencing a
// shared_ptr is a function call, and clang rightly points out that
// typeid evaluates its operand when the type is polymorphic. It is
// meant to - the runtime type is the whole point - and saying so this
// way asks for it without the warning.
const XCreature& creature = *original;

std::cout << "TestRealCreature: runtime type is " << typeid(creature).name() << std::endl;

const auto original_guid = original->guid();
const auto original_contain_size = original->contain.size();
Expand Down Expand Up @@ -454,9 +461,19 @@ static bool TestRealScheduler()
auto entry = restored.Get();
pass = pass && entry && dynamic_cast<XMoney*>(entry.get()) != nullptr;

// See TestRealCreature() above for why this goes through a reference.
// The name() a type_info hands back outlives everything here, so
// holding the pointer is safe.
const char* entry_type = "none";

if (entry) {
const XObject& object = *entry;
entry_type = typeid(object).name();
}

std::cout
<< " scheduler time " << local_sched.GetTime() << " -> " << restored.GetTime()
<< ", entry " << (entry ? typeid(*entry).name() : "none")
<< ", entry " << entry_type
<< std::endl;

// Genuine orphan (Cereal's plain-delete deleter, no Invalidate()
Expand Down
Loading
Loading