Compare commits

...

5 Commits

Author SHA1 Message Date
Ryan Walters
897b9b8621 fix: switch from lcov to codecov.json for Codecov reporting 2025-09-06 12:15:07 -05:00
Ryan Walters
ee2569b70c ci: drop coveralls, add codecov config, change badge 2025-09-06 12:15:07 -05:00
Ryan Walters
84caa6c25f ci: setup codecov coverage 2025-09-06 12:15:06 -05:00
Ryan Walters
f92c9175b9 test: add ttf renderer tests 2025-09-06 12:15:06 -05:00
Ryan Walters
d561b446c5 test: remove useless/redundant tests 2025-09-06 12:15:05 -05:00
9 changed files with 157 additions and 161 deletions

View File

@@ -9,8 +9,6 @@ env:
jobs:
coverage:
runs-on: ubuntu-latest
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
steps:
- name: Checkout code
uses: actions/checkout@v5
@@ -48,35 +46,11 @@ jobs:
- name: Generate coverage report
run: |
just coverage
just coverage-codecov
- name: Download Coveralls CLI
if: ${{ env.COVERALLS_REPO_TOKEN != '' }}
run: |
# use GitHub Releases URL instead of coveralls.io because they can't maintain their own files; it 404s
curl -L https://github.com/coverallsapp/coverage-reporter/releases/download/v0.6.15/coveralls-linux-x86_64.tar.gz | tar -xz -C /usr/local/bin
- name: Upload coverage to Coveralls
if: ${{ env.COVERALLS_REPO_TOKEN != '' }}
run: |
if [ ! -f "lcov.info" ]; then
echo "Error: lcov.info file not found. Coverage generation may have failed."
exit 1
fi
for i in {1..10}; do
echo "Attempt $i: Uploading coverage to Coveralls..."
if coveralls -n report lcov.info; then
echo "Successfully uploaded coverage report."
exit 0
fi
if [ $i -lt 10 ]; then
delay=$((2**i))
echo "Attempt $i failed. Retrying in $delay seconds..."
sleep $delay
fi
done
echo "Failed to upload coverage report after 10 attempts."
exit 1
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./codecov.json
disable_search: true

View File

@@ -11,7 +11,7 @@ binary_extension := if os() == "windows" { ".exe" } else { "" }
# !!! --remap-path-prefix prevents the absolute path from being used in the generated report
# Generate HTML report (for humans, source line inspection)
html: coverage
html: coverage-lcov
cargo llvm-cov report \
--remap-path-prefix \
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
@@ -19,13 +19,13 @@ html: coverage
--open
# Display report (for humans)
report-coverage: coverage
report-coverage: coverage-lcov
cargo llvm-cov report \
--remap-path-prefix \
--ignore-filename-regex "{{ coverage_exclude_pattern }}"
# Run & generate report (for CI)
coverage:
# Run & generate LCOV report (as base report)
coverage-lcov:
cargo llvm-cov \
--lcov \
--remap-path-prefix \
@@ -34,6 +34,16 @@ coverage:
--profile coverage \
--no-fail-fast nextest
# Run & generate Codecov report (for CI)
coverage-codecov:
cargo llvm-cov \
--codecov \
--remap-path-prefix \
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
--output-path codecov.json \
--profile coverage \
--no-fail-fast nextest
# Profile the project using 'samply'
samply:
cargo build --profile profile

View File

@@ -1,16 +1,16 @@
# Pac-Man
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![If you're seeing this, Coveralls.io is broken again and it's not my fault.][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![Code Coverage][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
[badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml/badge.svg
[badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg
[badge-coverage]: https://coveralls.io/repos/github/Xevion/Pac-Man/badge.svg?branch=master
[badge-coverage]: https://codecov.io/github/Xevion/Pac-Man/branch/master/graph/badge.svg?token=R2RBYUQK3I
[badge-demo]: https://img.shields.io/github/deployments/Xevion/Pac-Man/github-pages?label=GitHub%20Pages
[badge-online-demo]: https://img.shields.io/badge/GitHub%20Pages-Demo-brightgreen
[badge-last-commit]: https://img.shields.io/github/last-commit/Xevion/Pac-Man
[build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml
[test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml
[coverage]: https://coveralls.io/github/Xevion/Pac-Man?branch=master
[coverage]: https://codecov.io/github/Xevion/Pac-Man
[demo]: https://xevion.github.io/Pac-Man/
[commits]: https://github.com/Xevion/Pac-Man/commits/master

17
codecov.yml Normal file
View File

@@ -0,0 +1,17 @@
coverage:
status:
project:
default:
target: 70%
patch:
default:
informational: true
ignore:
- "src/(?:bin|platform))/.+\\.rs"
- "src/(?:app|events|formatter)\\.rs"
comment:
layout: "reach,diff,flags,files,footer"
behavior: default
require_changes: false

View File

@@ -15,11 +15,3 @@ fn all_asset_paths_exist() {
assert_that(&metadata.len()).is_greater_than(1024);
}
}
#[test]
fn asset_paths_are_non_empty() {
for asset in Asset::iter() {
let path = asset.path();
assert_that(&path.is_empty()).is_false();
}
}

View File

@@ -1,24 +0,0 @@
use pacman::constants::*;
use speculoos::prelude::*;
#[test]
fn test_raw_board_structure() {
// Test board dimensions match expected size
assert_that(&RAW_BOARD.len()).is_equal_to(BOARD_CELL_SIZE.y as usize);
for row in RAW_BOARD.iter() {
assert_that(&row.len()).is_equal_to(BOARD_CELL_SIZE.x as usize);
}
// Test boundaries are properly walled
assert_that(&RAW_BOARD[0].chars().all(|c| c == '#')).is_true();
assert_that(&RAW_BOARD[RAW_BOARD.len() - 1].chars().all(|c| c == '#')).is_true();
}
#[test]
fn test_raw_board_contains_required_elements() {
// Test that essential game elements are present
assert_that(&RAW_BOARD.iter().any(|row| row.contains('X'))).is_true();
assert_that(&RAW_BOARD.iter().any(|row| row.contains("=="))).is_true();
assert_that(&RAW_BOARD.iter().any(|row| row.chars().any(|c| c == 'T'))).is_true();
assert_that(&RAW_BOARD.iter().any(|row| row.chars().any(|c| c == 'o'))).is_true();
}

View File

@@ -1,76 +1,7 @@
use pacman::error::{
AssetError, EntityError, GameError, GameResult, IntoGameError, MapError, OptionExt, ParseError, ResultExt, TextureError,
};
use pacman::error::{GameError, GameResult, IntoGameError, OptionExt, ResultExt};
use speculoos::prelude::*;
use std::io;
#[test]
fn test_game_error_from_asset_error() {
let asset_error = AssetError::NotFound("test.png".to_string());
let game_error: GameError = asset_error.into();
assert_that(&matches!(game_error, GameError::Asset(_))).is_true();
}
#[test]
fn test_game_error_from_parse_error() {
let parse_error = ParseError::UnknownCharacter('Z');
let game_error: GameError = parse_error.into();
assert_that(&matches!(game_error, GameError::MapParse(_))).is_true();
}
#[test]
fn test_game_error_from_map_error() {
let map_error = MapError::NodeNotFound(42);
let game_error: GameError = map_error.into();
assert_that(&matches!(game_error, GameError::Map(_))).is_true();
}
#[test]
fn test_game_error_from_texture_error() {
let texture_error = TextureError::LoadFailed("Failed to load".to_string());
let game_error: GameError = texture_error.into();
assert_that(&matches!(game_error, GameError::Texture(_))).is_true();
}
#[test]
fn test_game_error_from_entity_error() {
let entity_error = EntityError::NodeNotFound(10);
let game_error: GameError = entity_error.into();
assert_that(&matches!(game_error, GameError::Entity(_))).is_true();
}
#[test]
fn test_game_error_from_io_error() {
let io_error = io::Error::new(io::ErrorKind::NotFound, "File not found");
let game_error: GameError = io_error.into();
assert_that(&matches!(game_error, GameError::Io(_))).is_true();
}
#[test]
fn test_asset_error_from_io_error() {
let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "Permission denied");
let asset_error: AssetError = io_error.into();
assert_that(&matches!(asset_error, AssetError::Io(_))).is_true();
}
#[test]
fn test_parse_error_display() {
let error = ParseError::UnknownCharacter('!');
assert_that(&error.to_string()).is_equal_to("Unknown character in board: !".to_string());
let error = ParseError::InvalidHouseDoorCount(3);
assert_that(&error.to_string()).is_equal_to("House door must have exactly 2 positions, found 3".to_string());
}
#[test]
fn test_entity_error_display() {
let error = EntityError::NodeNotFound(42);
assert_that(&error.to_string()).is_equal_to("Node not found in graph: 42".to_string());
let error = EntityError::EdgeNotFound { from: 1, to: 2 };
assert_that(&error.to_string()).is_equal_to("Edge not found: from 1 to 2".to_string());
}
#[test]
fn test_into_game_error_trait() {
let result: Result<i32, io::Error> = Err(io::Error::new(io::ErrorKind::Other, "test error"));

View File

@@ -73,25 +73,6 @@ fn test_default_zero_timing_for_unused_systems() {
}
}
#[test]
fn test_pre_populated_timing_entries() {
let timings = SystemTimings::default();
// Verify that we can add timing to any SystemId without panicking
// (this would fail with the old implementation if the entry didn't exist)
// Use the same tick for all systems to avoid zero-padding
for id in SystemId::iter() {
timings.add_timing(id, Duration::from_nanos(1), 1);
}
// Verify all systems now have non-zero timing
let stats = timings.get_stats(1);
for id in SystemId::iter() {
let (avg, _) = stats.get(&id).unwrap();
assert_that(&(*avg > Duration::ZERO)).is_true();
}
}
#[test]
fn test_total_system_timing() {
let timings = SystemTimings::default();

115
tests/ttf.rs Normal file
View File

@@ -0,0 +1,115 @@
use pacman::texture::ttf::{TtfAtlas, TtfRenderer};
use sdl2::pixels::Color;
mod common;
#[test]
fn text_width_calculates_correctly_for_empty_string() {
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
let _ttf_context = sdl2::ttf::init().unwrap();
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
let renderer = TtfRenderer::new(1.0);
let width = renderer.text_width(&atlas, "");
assert_eq!(width, 0);
}
#[test]
fn text_width_calculates_correctly_for_single_character() {
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
let _ttf_context = sdl2::ttf::init().unwrap();
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
let renderer = TtfRenderer::new(1.0);
let width = renderer.text_width(&atlas, "A");
assert!(width > 0);
}
#[test]
fn text_width_scales_correctly() {
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
let _ttf_context = sdl2::ttf::init().unwrap();
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
let renderer1 = TtfRenderer::new(1.0);
let renderer2 = TtfRenderer::new(2.0);
let width1 = renderer1.text_width(&atlas, "Test");
let width2 = renderer2.text_width(&atlas, "Test");
assert_eq!(width2, width1 * 2);
}
#[test]
fn text_height_returns_non_zero_for_valid_atlas() {
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
let _ttf_context = sdl2::ttf::init().unwrap();
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
let renderer = TtfRenderer::new(1.0);
let height = renderer.text_height(&atlas);
assert!(height > 0);
}
#[test]
fn text_height_scales_correctly() {
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
let _ttf_context = sdl2::ttf::init().unwrap();
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
let renderer1 = TtfRenderer::new(1.0);
let renderer2 = TtfRenderer::new(2.0);
let height1 = renderer1.text_height(&atlas);
let height2 = renderer2.text_height(&atlas);
assert_eq!(height2, height1 * 2);
}
#[test]
fn render_text_handles_empty_string() {
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
let _ttf_context = sdl2::ttf::init().unwrap();
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
let renderer = TtfRenderer::new(1.0);
let result = renderer.render_text(&mut canvas, &mut atlas, "", glam::Vec2::new(0.0, 0.0), Color::WHITE);
assert!(result.is_ok());
}
#[test]
fn render_text_handles_single_character() {
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
let _ttf_context = sdl2::ttf::init().unwrap();
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
let renderer = TtfRenderer::new(1.0);
let result = renderer.render_text(&mut canvas, &mut atlas, "A", glam::Vec2::new(10.0, 10.0), Color::RED);
assert!(result.is_ok());
}