diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca37570 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# ---> Vim +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +subprojects +builddir +ttassets +backup +*.exe +*.dll +*.world +coverage +coverage/* +.venv diff --git a/.tarpit.json b/.tarpit.json new file mode 100644 index 0000000..2cc4841 --- /dev/null +++ b/.tarpit.json @@ -0,0 +1,5 @@ +{ + "git_path": ".\\", + "build_cmd": "C:/Users/lcthw/AppData/Local/Microsoft/WinGet/Packages/ezwinports.make_Microsoft.Winget.Source_8wekyb3d8bbwe/bin/make.exe build", + "test_cmd": "./builddir/runtests.exe" +} diff --git a/.vimrc_proj b/.vimrc_proj new file mode 100644 index 0000000..2b745b4 --- /dev/null +++ b/.vimrc_proj @@ -0,0 +1 @@ +set makeprg=meson\ compile\ -C\ . diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a71da8c --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +all: build + +reset: +ifeq '$(OS)' 'Windows_NT' + powershell -executionpolicy bypass .\scripts\reset_build.ps1 +else + sh -x ./scripts/reset_build.sh +endif + +build: + meson compile -j 10 -C builddir + +release_build: + meson --wipe builddir -Db_ndebug=true --buildtype release + meson compile -j 10 -C builddir + +debug_build: + meson setup --wipe builddir -Db_ndebug=true --buildtype debugoptimized + meson compile -j 10 -C builddir + +tracy_build: + meson setup --wipe builddir --buildtype debugoptimized -Dtracy_enable=true -Dtracy:on_demand=true + meson compile -j 10 -C builddir + +run: build +ifeq '$(OS)' 'Windows_NT' + powershell "cp ./builddir/game.exe ." + ./game +else + ./builddir/game +endif + +debug: build + gdb --nx -x .gdbinit --ex run --args builddir/clicker + +debug_run: build + gdb --nx -x .gdbinit --batch --ex run --ex bt --ex q --args builddir/clicker + +clean: + meson compile --clean -C builddir + +debug_test: build + gdb --nx -x .gdbinit --ex run --args builddir/runtests -e + +win_installer: + powershell 'start "C:\Program Files (x86)\solicus\InstallForge\bin\ifbuilderenvx86.exe" scripts\win_installer.ifp' + +coverage_report: + powershell 'scripts/coverage_report.ps1' diff --git a/assets/clicker_the_dog-1024.png b/assets/clicker_the_dog-1024.png new file mode 100644 index 0000000..e25e06b Binary files /dev/null and b/assets/clicker_the_dog-1024.png differ diff --git a/assets/clicker_treat_bone.png b/assets/clicker_treat_bone.png new file mode 100644 index 0000000..afe9d42 Binary files /dev/null and b/assets/clicker_treat_bone.png differ diff --git a/assets/config.json b/assets/config.json new file mode 100644 index 0000000..30e1539 --- /dev/null +++ b/assets/config.json @@ -0,0 +1,28 @@ +{ + "sounds": { + "ui_click": "assets/sounds/ui_click.ogg", + "ui_hover": "assets/sounds/ui_hover.ogg", + "clicker_bark": "assets/sounds/clicker_bark.ogg", + "blank": "assets/sounds/blank.ogg" + }, + "sprites": { + "textures_test": + {"path": "assets/textures_test.png", + "frame_width": 53, + "frame_height": 34 + }, + "clicker_the_dog": + {"path": "assets/clicker_the_dog-1024.png", + "frame_width": 1024, + "frame_height": 1024 + }, + "clicker_treat_bone": + {"path": "assets/clicker_treat_bone.png", + "frame_width": 256, + "frame_height": 144 + } + }, + "graphics": { + "smooth_textures": false + } +} diff --git a/assets/shaders.json b/assets/shaders.json new file mode 100644 index 0000000..825dbba --- /dev/null +++ b/assets/shaders.json @@ -0,0 +1,10 @@ +{ + "ui_shader": { + "file_name": "assets/shaders/ui_shader.frag", + "type": "fragment" + }, + "ERROR": { + "file_name": "assets/shaders/ui_error.frag", + "type": "fragment" + } +} diff --git a/assets/shaders/ui_error.frag b/assets/shaders/ui_error.frag new file mode 100644 index 0000000..29ccc8c --- /dev/null +++ b/assets/shaders/ui_error.frag @@ -0,0 +1,18 @@ +uniform vec2 u_resolution; +uniform vec2 u_mouse; +uniform float u_duration; +uniform float u_time; +uniform float u_time_end; +uniform sampler2D texture; +uniform bool is_shape; + +void main() { + if(is_shape) { + vec4 color = vec4(1.0, 0.0, 0.0, 1.0); + gl_FragColor = gl_Color * color; + } else { + vec4 pixel = texture2D(texture, gl_TexCoord[0].xy); + vec4 color = vec4(1.0, 0.0, 0.0, 1.0); + gl_FragColor = gl_Color * color * pixel; + } +} diff --git a/assets/shaders/ui_shader.frag b/assets/shaders/ui_shader.frag new file mode 100644 index 0000000..73b77b4 --- /dev/null +++ b/assets/shaders/ui_shader.frag @@ -0,0 +1,29 @@ +uniform vec2 u_resolution; +uniform vec2 u_mouse; +uniform float u_duration; +uniform float u_time; +uniform float u_time_end; +uniform sampler2D texture; +uniform bool is_shape; +uniform bool hover; + +vec4 blink() { + if(hover) { + return vec4(0.95, 0.95, 1.0, 1.0); + } else { + float tick = (u_time_end - u_time) / u_duration; + float blink = mix(0.5, 1.0, tick); + return vec4(blink, blink, blink, 1.0); + } +} + +void main() { + vec4 color = blink(); + + if(!is_shape) { + vec4 pixel = texture2D(texture, gl_TexCoord[0].xy); + color *= pixel; + } + + gl_FragColor = gl_Color * color; +} diff --git a/assets/shaders/ui_shape_shader.frag b/assets/shaders/ui_shape_shader.frag new file mode 100644 index 0000000..c16d6ea --- /dev/null +++ b/assets/shaders/ui_shape_shader.frag @@ -0,0 +1,12 @@ +uniform vec2 u_resolution; +uniform vec2 u_mouse; +uniform float u_duration; +uniform float u_time; +uniform float u_time_end; + +void main() { + float tick = (u_time_end - u_time) / u_duration; + float blink = smoothstep(1.0, 0.5, tick); + vec4 color = vec4(blink, blink, blink, 1.0); + gl_FragColor = gl_Color * color; +} diff --git a/assets/sounds/blank.ogg b/assets/sounds/blank.ogg new file mode 100644 index 0000000..3322d4b Binary files /dev/null and b/assets/sounds/blank.ogg differ diff --git a/assets/sounds/clicker_bark.ogg b/assets/sounds/clicker_bark.ogg new file mode 100644 index 0000000..305661f Binary files /dev/null and b/assets/sounds/clicker_bark.ogg differ diff --git a/assets/sounds/ui_click.ogg b/assets/sounds/ui_click.ogg new file mode 100644 index 0000000..7ff2e8c Binary files /dev/null and b/assets/sounds/ui_click.ogg differ diff --git a/assets/sounds/ui_hover.ogg b/assets/sounds/ui_hover.ogg new file mode 100644 index 0000000..be6e679 Binary files /dev/null and b/assets/sounds/ui_hover.ogg differ diff --git a/assets/text.otf b/assets/text.otf new file mode 100644 index 0000000..3094772 Binary files /dev/null and b/assets/text.otf differ diff --git a/assets/textures_test.png b/assets/textures_test.png new file mode 100644 index 0000000..1ffb41e Binary files /dev/null and b/assets/textures_test.png differ diff --git a/backend.cpp b/backend.cpp new file mode 100644 index 0000000..81eaa5b --- /dev/null +++ b/backend.cpp @@ -0,0 +1,69 @@ +#include "backend.hpp" +#include +#include +#include +#include + +namespace sfml { + guecs::SpriteTexture Backend::texture_get(const string& name) { + auto sp = textures::get(name); + return {sp.sprite, sp.texture}; + } + + Backend::Backend() { + sound::init(); + shaders::init(); + textures::init(); + } + + void Backend::sound_play(const string& name) { + sound::play(name); + } + + void Backend::sound_stop(const string& name) { + sound::stop(name); + } + + std::shared_ptr Backend::shader_get(const std::string& name) { + return shaders::get(name); + } + + bool Backend::shader_updated() { + if(shaders::updated($shaders_version)) { + $shaders_version = shaders::version(); + return true; + } else { + return false; + } + } + + guecs::Theme Backend::theme() { + guecs::Theme theme; + + // { + // .BLACK={1, 4, 2}, + // .DARK_DARK={9, 29, 16}, + // .DARK_MID={14, 50, 26}, + // .DARK_LIGHT={0, 109, 44}, + // .MID={63, 171, 92}, + // .LIGHT_DARK={161, 217, 155}, + // .LIGHT_MID={199, 233, 192}, + // .LIGHT_LIGHT={229, 245, 224}, + // .WHITE={255, 255, 255}, + // .TRANSPARENT = sf::Color::Transparent + // }; + + theme.PADDING = 3; + theme.BORDER_PX = 1; + theme.TEXT_SIZE = 40; + theme.LABEL_SIZE = 40; + theme.FILL_COLOR = theme.DARK_DARK; + theme.TEXT_COLOR = theme.LIGHT_LIGHT; + theme.BG_COLOR = theme.MID; + theme.BORDER_COLOR = theme.LIGHT_DARK; + theme.BG_COLOR_DARK = theme.BLACK; + theme.FONT_FILE_NAME = Config::path_to("assets/text.otf").string(); + + return theme; + } +} diff --git a/backend.hpp b/backend.hpp new file mode 100644 index 0000000..fed18f8 --- /dev/null +++ b/backend.hpp @@ -0,0 +1,19 @@ +#include "guecs/ui.hpp" + +namespace sfml { + using std::string; + + class Backend : public guecs::Backend { + int $shaders_version = 0; + + public: + + Backend(); + guecs::SpriteTexture texture_get(const string& name); + void sound_play(const string& name); + void sound_stop(const string& name); + std::shared_ptr shader_get(const std::string& name); + bool shader_updated(); + guecs::Theme theme(); + }; +} diff --git a/dbc.cpp b/dbc.cpp new file mode 100644 index 0000000..6b17faf --- /dev/null +++ b/dbc.cpp @@ -0,0 +1,47 @@ +#include "dbc.hpp" +#include + +void dbc::log(const string &message, const std::source_location location) { + std::cout << '[' << location.file_name() << ':' + << location.line() << "|" + << location.function_name() << "] " + << message << std::endl; +} + +void dbc::sentinel(const string &message, const std::source_location location) { + string err = fmt::format("[SENTINEL!] {}", message); + dbc::log(err, location); + throw dbc::SentinelError{err}; +} + +void dbc::pre(const string &message, bool test, const std::source_location location) { + if(!test) { + string err = fmt::format("[PRE!] {}", message); + dbc::log(err, location); + throw dbc::PreCondError{err}; + } +} + +void dbc::pre(const string &message, std::function tester, const std::source_location location) { + dbc::pre(message, tester(), location); +} + +void dbc::post(const string &message, bool test, const std::source_location location) { + if(!test) { + string err = fmt::format("[POST!] {}", message); + dbc::log(err, location); + throw dbc::PostCondError{err}; + } +} + +void dbc::post(const string &message, std::function tester, const std::source_location location) { + dbc::post(message, tester(), location); +} + +void dbc::check(bool test, const string &message, const std::source_location location) { + if(!test) { + string err = fmt::format("[CHECK!] {}\n", message); + dbc::log(err, location); + throw dbc::CheckError{err}; + } +} diff --git a/dbc.hpp b/dbc.hpp new file mode 100644 index 0000000..d090503 --- /dev/null +++ b/dbc.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include + + +namespace dbc { + using std::string; + + class Error { + public: + const string message; + Error(string m) : message{m} {} + Error(const char *m) : message{m} {} + }; + + class CheckError : public Error {}; + class SentinelError : public Error {}; + class PreCondError : public Error {}; + class PostCondError : public Error {}; + + void log(const string &message, + const std::source_location location = + std::source_location::current()); + + [[noreturn]] void sentinel(const string &message, + const std::source_location location = + std::source_location::current()); + + void pre(const string &message, bool test, + const std::source_location location = + std::source_location::current()); + + void pre(const string &message, std::function tester, + const std::source_location location = + std::source_location::current()); + + void post(const string &message, bool test, + const std::source_location location = + std::source_location::current()); + + void post(const string &message, std::function tester, + const std::source_location location = + std::source_location::current()); + + void check(bool test, const string &message, + const std::source_location location = + std::source_location::current()); +} diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..b7d7c56 --- /dev/null +++ b/main.cpp @@ -0,0 +1,177 @@ +#include "guecs/sfml/backend.hpp" +#include "guecs/sfml/components.hpp" +#include "guecs/ui.hpp" +#include +#include +#include + +constexpr const int WINDOW_WIDTH=1280; +constexpr const int WINDOW_HEIGHT=720; +constexpr const int FRAME_LIMIT=60; +constexpr const bool VSYNC=true; + +using std::string, std::wstring; + +enum class Event { + CLICKER, A_BUTTON +}; + +struct Shake { + float scale_factor = 0.05f; + int frames = 10; + float ease_rate = 0.1f; + bool playing = false; + int current = 0; + float x=0.0; + float y=0.0; + float w=0.0; + float h=0.0; + sf::Vector2f initial_scale; + + float ease() { + float tick = float(frames) / float(current) * ease_rate; + return (std::sin(tick) + 1.0) / 2.0; + } + + void init(lel::Cell& cell) { + x = cell.x; + y = cell.y; + w = cell.w; + h = cell.h; + } + + void play(guecs::Sprite& sprite) { + if(!playing) { + playing = true; + current = 0; + initial_scale = sprite.sprite->getScale(); + } + } + + void render(guecs::Sprite& sprite) { + current++; + + if(playing && current < frames) { + float tick = ease(); + sf::Vector2f scale{ + std::lerp(initial_scale.x, initial_scale.x + scale_factor, tick), + std::lerp(initial_scale.y, initial_scale.y + scale_factor, tick)}; + + sprite.sprite->setScale(scale); + } else { + playing = false; + current = 0; + sprite.sprite->setScale(initial_scale); + } + } +}; + +struct ClickerUI { + guecs::UI $gui; + guecs::Entity $clicker; + + ClickerUI() { + $gui.position(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT); + $gui.layout( + "[_|*%(300,400)clicker|_|_|_]" + "[_|_ |_|_|_]" + "[_|_ |_|_|_]" + "[_|_ |_|_|_]" + "[a1|a2|a3|a4|a5]"); + } + + void init() { + $gui.set($gui.MAIN, {$gui.$parser, {0, 0, 0, 255}}); + + for(auto& [name, cell] : $gui.cells()) { + auto id = $gui.entity(name); + if(name != "clicker") { + $gui.set(id, {}); + $gui.set(id, {}); + $gui.set(id, { "clicker_treat_bone" }); + fmt::println("button dim: {},{}", cell.w, cell.h); + $gui.set(id, { + [&](auto, auto) { handle_button(Event::A_BUTTON); } + }); + } + } + + $clicker = $gui.entity("clicker"); + $gui.set($clicker, {"clicker_the_dog"}); + $gui.set($clicker, {"clicker_bark"}); + $gui.set($clicker, { + [&](auto, auto) { handle_button(Event::CLICKER); } + }); + + // custom components need to be initialized manually + $gui.set_init($clicker, {}); + + $gui.init(); + } + + void render(sf::RenderWindow& window) { + auto& shaker = $gui.get($clicker); + + if(shaker.playing) { + auto& sprite = $gui.get($clicker); + shaker.render(sprite); + window.clear(); + } + + $gui.render(window); + + // $gui.debug_layout(window); + } + + void mouse(float x, float y, bool hover) { + $gui.mouse(x, y, hover); + } + + void handle_button(Event ev) { + using enum Event; + switch(ev) { + case CLICKER: { + auto& shaker = $gui.get($clicker); + auto& sprite = $gui.get($clicker); + shaker.play(sprite); + fmt::println("CLICKER LOVES YOU!"); + } break; + case A_BUTTON: + fmt::println("a button clicked"); + break; + + default: + assert(false && "invalid event"); + } + } +}; + +int main() { + sfml::Backend backend; + guecs::init(&backend); + + sf::RenderWindow window(sf::VideoMode({WINDOW_WIDTH, WINDOW_HEIGHT}), "Clicker the Dog"); + window.setFramerateLimit(FRAME_LIMIT); + window.setVerticalSyncEnabled(VSYNC); + + ClickerUI clicker; + clicker.init(); + + while(window.isOpen()) { + while (const auto event = window.pollEvent()) { + if(event->is()) { + window.close(); + } + + if(const auto* mouse = event->getIf()) { + if(mouse->button == sf::Mouse::Button::Left) { + sf::Vector2f pos = window.mapPixelToCoords(mouse->position); + clicker.mouse(pos.x, pos.y, false); + } + } + } + + clicker.render(window); + window.display(); + } +} diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..5463d5d --- /dev/null +++ b/meson.build @@ -0,0 +1,102 @@ +# clang might need _LIBCPP_ENABLE_CXX26_REMOVED_CODECVT + +project('lel-sfml-starter', 'cpp', + version: '0.1.0', + default_options: [ + 'cpp_std=c++20', + 'cpp_args=-D_GLIBCXX_DEBUG=1 -D_GLIBCXX_DEBUG_PEDANTIC=1', + ]) + +# use this for common options only for our executables +cpp_args=[] +link_args=[] +# these are passed as override_defaults +exe_defaults = [ 'warning_level=2' ] + +cc = meson.get_compiler('cpp') +dependencies = [] + +if build_machine.system() == 'windows' + add_global_link_arguments( + '-static-libgcc', + '-static-libstdc++', + '-static', + language: 'cpp', + ) + + sfml_main = dependency('sfml_main') + opengl32 = cc.find_library('opengl32', required: true) + winmm = cc.find_library('winmm', required: true) + gdi32 = cc.find_library('gdi32', required: true) + + dependencies += [ + opengl32, winmm, gdi32, sfml_main + ] + exe_defaults += ['werror=true'] + +elif build_machine.system() == 'darwin' + add_global_link_arguments( + language: 'cpp', + ) + + opengl = dependency('OpenGL') + corefoundation = dependency('CoreFoundation') + carbon = dependency('Carbon') + cocoa = dependency('Cocoa') + iokit = dependency('IOKit') + corevideo = dependency('CoreVideo') + + link_args += ['-ObjC'] + exe_defaults += ['werror=false'] + dependencies += [ + opengl, corefoundation, carbon, cocoa, iokit, corevideo + ] +endif + +catch2 = subproject('catch2').get_variable('catch2_with_main_dep') +fmt = subproject('fmt').get_variable('fmt_dep') +json = subproject('nlohmann_json').get_variable('nlohmann_json_dep') +freetype2 = subproject('freetype2').get_variable('freetype_dep') + +flac = subproject('flac').get_variable('flac_dep') +ogg = subproject('ogg').get_variable('libogg_dep') +vorbis = subproject('vorbis').get_variable('vorbis_dep') +vorbisfile = subproject('vorbis').get_variable('vorbisfile_dep') +vorbisenc = subproject('vorbis').get_variable('vorbisenc_dep') +sfml_audio = subproject('sfml').get_variable('sfml_audio_dep') +sfml_graphics = subproject('sfml').get_variable('sfml_graphics_dep') +sfml_network = subproject('sfml').get_variable('sfml_network_dep') +sfml_system = subproject('sfml').get_variable('sfml_system_dep') +sfml_window = subproject('sfml').get_variable('sfml_window_dep') +lel_guecs = subproject('lel-guecs').get_variable('lel_guecs_dep') +lel_guecs_sfml = subproject('lel-guecs').get_variable('lel_guecs_sfml_dep') + +dependencies += [ + fmt, json, freetype2, + flac, ogg, vorbis, vorbisfile, vorbisenc, + sfml_audio, sfml_graphics, + sfml_network, sfml_system, + sfml_window, lel_guecs, lel_guecs_sfml +] + +sources = [ + 'dbc.cpp', + 'backend.cpp', + 'main.cpp', +] + +tests = [ + 'tests/sample.cpp' +] + +executable('game', sources, + cpp_args: cpp_args, + link_args: link_args, + override_options: exe_defaults, + dependencies: dependencies) + +executable('runtests', sources + tests, + cpp_args: cpp_args, + link_args: link_args, + override_options: exe_defaults, + dependencies: dependencies + [catch2]) diff --git a/scripts/coverage_report.ps1 b/scripts/coverage_report.ps1 new file mode 100644 index 0000000..79e38de --- /dev/null +++ b/scripts/coverage_report.ps1 @@ -0,0 +1,13 @@ +rm -recurse -force coverage/* +cp *.cpp,*.hpp,*.rl builddir + +. .venv/Scripts/activate + +rm -recurse -force coverage +cp scripts\gcovr_patched_coverage.py .venv\Lib\site-packages\gcovr\coverage.py + +gcovr -o coverage/ --html --html-details --html-theme github.dark-blue --gcov-ignore-errors all --gcov-ignore-parse-errors negative_hits.warn_once_per_file -e builddir/subprojects -e builddir -e subprojects -j 10 . + +rm *.gcov.json.gz + +start .\coverage\coverage_details.html diff --git a/scripts/coverage_reset.ps1 b/scripts/coverage_reset.ps1 new file mode 100644 index 0000000..4799ae6 --- /dev/null +++ b/scripts/coverage_reset.ps1 @@ -0,0 +1,7 @@ +mv .\subprojects\packagecache . +rm -recurse -force .\subprojects\,.\builddir\ +mkdir subprojects +mv .\packagecache .\subprojects\ +mkdir builddir +cp wraps\*.wrap subprojects\ +meson setup --default-library=static --prefer-static -Db_coverage=true builddir diff --git a/scripts/coverage_reset.sh b/scripts/coverage_reset.sh new file mode 100644 index 0000000..9970738 --- /dev/null +++ b/scripts/coverage_reset.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e + +mv -f ./subprojects/packagecache . +rm -rf subprojects builddir +mkdir subprojects +mv packagecache ./subprojects/ +mkdir builddir +cp wraps/*.wrap subprojects/ +# on OSX you can't do this with static +meson setup -Db_coverage=true builddir diff --git a/scripts/gcovr_patched_coverage.py b/scripts/gcovr_patched_coverage.py new file mode 100644 index 0000000..7baecca --- /dev/null +++ b/scripts/gcovr_patched_coverage.py @@ -0,0 +1,1020 @@ +# -*- coding:utf-8 -*- + +# ************************** Copyrights and license *************************** +# +# This file is part of gcovr 8.3, a parsing and reporting tool for gcov. +# https://gcovr.com/en/8.3 +# +# _____________________________________________________________________________ +# +# Copyright (c) 2013-2025 the gcovr authors +# Copyright (c) 2013 Sandia Corporation. +# Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation, +# the U.S. Government retains certain rights in this software. +# +# This software is distributed under the 3-clause BSD License. +# For more information, see the README.rst file. +# +# **************************************************************************** + +""" +The gcovr coverage data model. + +This module represents the core data structures +and should not have dependencies on any other gcovr module, +also not on the gcovr.utils module. + +The data model should contain the exact same information +as the JSON input/output format. + +The types ending with ``*Coverage`` +contain per-project/-line/-decision/-branch coverage. + +The types ``SummarizedStats``, ``CoverageStat``, and ``DecisionCoverageStat`` +report aggregated metrics/percentages. +""" + +from __future__ import annotations +import logging +import os +import re +from typing import ( + ItemsView, + Iterator, + Iterable, + Optional, + TypeVar, + Union, + Literal, + ValuesView, +) +from dataclasses import dataclass + +from .utils import commonpath, force_unix_separator + +LOGGER = logging.getLogger("gcovr") + +_T = TypeVar("_T") + + +def sort_coverage( + covdata: Union[ + dict[str, FileCoverage], + dict[str, Union[FileCoverage, CoverageContainerDirectory]], + ], + sort_key: Literal["filename", "uncovered-number", "uncovered-percent"], + sort_reverse: bool, + by_metric: Literal["line", "branch", "decision"], + filename_uses_relative_pathname: bool = False, +) -> list[str]: + """Sort a coverage dict. + + covdata (dict): the coverage dictionary + sort_key ("filename", "uncovered-number", "uncovered-percent"): the values to sort by + sort_reverse (bool): reverse order if True + by_metric ("line", "branch", "decision"): select the metric to sort + filename_uses_relative_pathname (bool): for html, we break down a pathname to the + relative path, but not for other formats. + + returns: the sorted keys + """ + + basedir = commonpath(list(covdata.keys())) + + def key_filename(key: str) -> list[Union[int, str]]: + def convert_to_int_if_possible(text: str) -> Union[int, str]: + return int(text) if text.isdigit() else text + + key = ( + force_unix_separator( + os.path.relpath(os.path.realpath(key), os.path.realpath(basedir)) + ) + if filename_uses_relative_pathname + else key + ).casefold() + + return [convert_to_int_if_possible(part) for part in re.split(r"([0-9]+)", key)] + + def coverage_stat(key: str) -> CoverageStat: + cov = covdata[key] + if by_metric == "branch": + return cov.branch_coverage() + if by_metric == "decision": + return cov.decision_coverage().to_coverage_stat + return cov.line_coverage() + + def key_num_uncovered(key: str) -> int: + stat = coverage_stat(key) + uncovered = stat.total - stat.covered + return uncovered + + def key_percent_uncovered(key: str) -> float: + stat = coverage_stat(key) + covered = stat.covered + total = stat.total + + # No branches are always put directly after (or before when reversed) + # files with 100% coverage (by assigning such files 110% coverage) + return covered / total if total > 0 else 1.1 + + if sort_key == "uncovered-number": + # First sort filename alphabetical and then by the requested key + return sorted( + sorted(covdata, key=key_filename), + key=key_num_uncovered, + reverse=sort_reverse, + ) + if sort_key == "uncovered-percent": + # First sort filename alphabetical and then by the requested key + return sorted( + sorted(covdata, key=key_filename), + key=key_percent_uncovered, + reverse=sort_reverse, + ) + + # By default, we sort by filename alphabetically + return sorted(covdata, key=key_filename, reverse=sort_reverse) + + +class BranchCoverage: + r"""Represent coverage information about a branch. + + Args: + source_block_id (int): + The block number. + count (int): + Number of times this branch was followed. + fallthrough (bool, optional): + Whether this is a fallthrough branch. False if unknown. + throw (bool, optional): + Whether this is an exception-handling branch. False if unknown. + destination_block_id (int, optional): + The destination block of the branch. None if unknown. + excluded (bool, optional): + Whether the branch is excluded. + """ + + first_undefined_source_block_id: bool = True + + __slots__ = ( + "source_block_id", + "count", + "fallthrough", + "throw", + "destination_block_id", + "excluded", + ) + + def __init__( + self, + source_block_id: Optional[int], + count: int, + fallthrough: bool = False, + throw: bool = False, + destination_block_id: Optional[int] = None, + excluded: Optional[bool] = None, + ) -> None: + if count < 0: + raise AssertionError("count must not be a negative value.") + + self.source_block_id = source_block_id + self.count = count + self.fallthrough = fallthrough + self.throw = throw + self.destination_block_id = destination_block_id + self.excluded = excluded + + @property + def source_block_id_or_0(self) -> int: + """Get a valid block number (0) if there was no definition in GCOV file.""" + if self.source_block_id is None: + self.source_block_id = 0 + if BranchCoverage.first_undefined_source_block_id: + BranchCoverage.first_undefined_source_block_id = False + LOGGER.info("No block number defined, assuming 0 for all undefined") + + return self.source_block_id + + @property + def is_excluded(self) -> bool: + """Return True if the branch is excluded.""" + return False if self.excluded is None else self.excluded + + @property + def is_reportable(self) -> bool: + """Return True if the branch is reportable.""" + return not self.excluded + + @property + def is_covered(self) -> bool: + """Return True if the branch is covered.""" + return self.is_reportable and self.count > 0 + + +class CallCoverage: + r"""Represent coverage information about a call. + + Args: + callno (int): + The number of the call. + covered (bool): + Whether the call was performed. + excluded (bool, optional): + Whether the call is excluded. + """ + + __slots__ = "callno", "covered", "excluded" + + def __init__( + self, + callno: int, + covered: bool, + excluded: Optional[bool] = False, + ) -> None: + self.callno = callno + self.covered = covered + self.excluded = excluded + + @property + def is_reportable(self) -> bool: + """Return True if the call is reportable.""" + return not self.excluded + + @property + def is_covered(self) -> bool: + """Return True if the call is covered.""" + return self.is_reportable and self.covered + + +class ConditionCoverage: + r"""Represent coverage information about a condition. + + Args: + count (int): + The number of the call. + covered (int): + Whether the call was performed. + not_covered_true list[int]: + The conditions which were not true. + not_covered_false list[int]: + The conditions which were not false. + excluded (bool, optional): + Whether the condition is excluded. + """ + + __slots__ = "count", "covered", "not_covered_true", "not_covered_false", "excluded" + + def __init__( + self, + count: int, + covered: int, + not_covered_true: list[int], + not_covered_false: list[int], + excluded: Optional[bool] = False, + ) -> None: + if count < 0: + raise AssertionError("count must not be a negative value.") + if count < covered: + raise AssertionError("count must not be less than covered.") + self.count = count + self.covered = covered + self.not_covered_true = not_covered_true + self.not_covered_false = not_covered_false + self.excluded = excluded + + +class DecisionCoverageUncheckable: + r"""Represent coverage information about a decision.""" + + __slots__ = () + + def __init__(self) -> None: + pass + + +class DecisionCoverageConditional: + r"""Represent coverage information about a decision. + + Args: + count_true (int): + Number of times this decision was made. + + count_false (int): + Number of times this decision was made. + + """ + + __slots__ = "count_true", "count_false" + + def __init__(self, count_true: int, count_false: int) -> None: + if count_true < 0: + raise AssertionError("count_true must not be a negative value.") + self.count_true = count_true + if count_false < 0: + raise AssertionError("count_true must not be a negative value.") + self.count_false = count_false + + +class DecisionCoverageSwitch: + r"""Represent coverage information about a decision. + + Args: + count (int): + Number of times this decision was made. + """ + + __slots__ = ("count",) + + def __init__(self, count: int) -> None: + if count < 0: + raise AssertionError("count must not be a negative value.") + self.count = count + + +DecisionCoverage = Union[ + DecisionCoverageConditional, + DecisionCoverageSwitch, + DecisionCoverageUncheckable, +] + + +class FunctionCoverage: + r"""Represent coverage information about a function. + + The counter is stored as dictionary with the line as key to be able + to merge function coverage in different ways + + Args: + name (str): + The mangled name of the function, None if not available. + demangled_name (str): + The demangled name (signature) of the functions. + lineno (int): + The line number. + count (int): + How often this function was executed. + blocks (float): + Block coverage of function. + start ((int, int)), optional): + Tuple with function start line and column. + end ((int, int)), optional): + Tuple with function end line and column. + excluded (bool, optional): + Whether this line is excluded by a marker. + """ + + __slots__ = ( + "name", + "demangled_name", + "count", + "blocks", + "start", + "end", + "excluded", + ) + + def __init__( + self, + name: Optional[str], + demangled_name: str, + *, + lineno: int, + count: int, + blocks: float, + start: Optional[tuple[int, int]] = None, + end: Optional[tuple[int, int]] = None, + excluded: bool = False, + ) -> None: + if count < 0: count = 0 + self.name = name + self.demangled_name = demangled_name + self.count = dict[int, int]({lineno: count}) + self.blocks = dict[int, float]({lineno: blocks}) + self.excluded = dict[int, bool]({lineno: excluded}) + self.start: Optional[dict[int, tuple[int, int]]] = ( + None if start is None else {lineno: start} + ) + self.end: Optional[dict[int, tuple[int, int]]] = ( + None if end is None else {lineno: end} + ) + + +class LineCoverage: + r"""Represent coverage information about a line. + + Each line is either *excluded* or *reportable*. + + A *reportable* line is either *covered* or *uncovered*. + + The default state of a line is *coverable*/*reportable*/*uncovered*. + + Args: + lineno (int): + The line number. + count (int): + How often this line was executed at least partially. + function_name (str, optional): + Mangled name of the function the line belongs to. + block_ids (*int, optional): + List of block ids in this line + excluded (bool, optional): + Whether this line is excluded by a marker. + md5 (str, optional): + The md5 checksum of the source code line. + """ + + __slots__ = ( + "lineno", + "count", + "function_name", + "block_ids", + "excluded", + "md5", + "branches", + "conditions", + "decision", + "calls", + ) + + def __init__( + self, + lineno: int, + count: int, + function_name: Optional[str] = None, + block_ids: Optional[list[int]] = None, + md5: Optional[str] = None, + excluded: bool = False, + ) -> None: + if lineno <= 0: + raise AssertionError("Line number must be a positive value.") + if count < 0: + raise AssertionError("count must not be a negative value.") + + self.lineno: int = lineno + self.count: int = count + self.function_name: Optional[str] = function_name + self.block_ids: Optional[list[int]] = block_ids + self.md5: Optional[str] = md5 + self.excluded: bool = excluded + self.branches = dict[int, BranchCoverage]() + self.conditions = dict[int, ConditionCoverage]() + self.decision: Optional[DecisionCoverage] = None + self.calls = dict[int, CallCoverage]() + + @property + def is_excluded(self) -> bool: + """Return True if the line is excluded.""" + return self.excluded + + @property + def is_reportable(self) -> bool: + """Return True if the line is reportable.""" + return not self.excluded + + @property + def is_covered(self) -> bool: + """Return True if the line is covered.""" + return self.is_reportable and self.count > 0 + + @property + def is_uncovered(self) -> bool: + """Return True if the line is uncovered.""" + return self.is_reportable and self.count == 0 + + @property + def has_uncovered_branch(self) -> bool: + """Return True if the line has a uncovered branches.""" + return not all( + branchcov.is_covered or branchcov.is_excluded + for branchcov in self.branches.values() + ) + + @property + def has_uncovered_decision(self) -> bool: + """Return True if the line has a uncovered decision.""" + if self.decision is None: + return False + + if isinstance(self.decision, DecisionCoverageUncheckable): + return False + + if isinstance(self.decision, DecisionCoverageConditional): + return self.decision.count_true == 0 or self.decision.count_false == 0 + + if isinstance(self.decision, DecisionCoverageSwitch): + return self.decision.count == 0 + + raise AssertionError(f"Unknown decision type: {self.decision!r}") + + def exclude(self) -> None: + """Exclude line from coverage statistic.""" + self.excluded = True + self.count = 0 + self.branches.clear() + self.conditions.clear() + self.decision = None + self.calls.clear() + + def branch_coverage(self) -> CoverageStat: + """Return the branch coverage statistic of the line.""" + total = 0 + covered = 0 + for branchcov in self.branches.values(): + if branchcov.is_reportable: + total += 1 + if branchcov.is_covered: + covered += 1 + return CoverageStat(covered=covered, total=total) + + def condition_coverage(self) -> CoverageStat: + """Return the condition coverage statistic of the line.""" + total = 0 + covered = 0 + for condition in self.conditions.values(): + total += condition.count + covered += condition.covered + return CoverageStat(covered=covered, total=total) + + def decision_coverage(self) -> DecisionCoverageStat: + """Return the decision coverage statistic of the line.""" + if self.decision is None: + return DecisionCoverageStat(0, 0, 0) + + if isinstance(self.decision, DecisionCoverageUncheckable): + return DecisionCoverageStat(0, 1, 2) # TODO should it be uncheckable=2? + + if isinstance(self.decision, DecisionCoverageConditional): + covered = 0 + if self.decision.count_true > 0: + covered += 1 + if self.decision.count_false > 0: + covered += 1 + return DecisionCoverageStat(covered, 0, 2) + + if isinstance(self.decision, DecisionCoverageSwitch): + covered = 0 + if self.decision.count > 0: + covered += 1 + return DecisionCoverageStat(covered, 0, 1) + + raise AssertionError(f"Unknown decision type: {self.decision!r}") + + +class FileCoverage: + """Represent coverage information about a file.""" + + __slots__ = "filename", "functions", "lines", "data_sources" + + def __init__( + self, filename: str, data_source: Optional[Union[str, set[str]]] + ) -> None: + self.filename: str = filename + self.functions = dict[str, FunctionCoverage]() + self.lines = dict[int, LineCoverage]() + self.data_sources = ( + set[str]() + if data_source is None + else set[str]( + [data_source] if isinstance(data_source, str) else data_source + ) + ) + + def filter_for_function(self, functioncov: FunctionCoverage) -> FileCoverage: + """Get a file coverage object reduced to a single function""" + if functioncov.name not in self.functions: + raise AssertionError( + f"Function {functioncov.name} must be in filtered file coverage object." + ) + if functioncov.name is None: + raise AssertionError( + "Data for filtering is missing. Need supported GCOV JSON format to get the information." + ) + filecov = FileCoverage(self.filename, self.data_sources) + filecov.functions[functioncov.name] = functioncov + + filecov.lines = { + lineno: linecov + for lineno, linecov in self.lines.items() + if linecov.function_name == functioncov.name + } + + return filecov + + @property + def stats(self) -> SummarizedStats: + """Create a coverage statistic of a file coverage object.""" + return SummarizedStats( + line=self.line_coverage(), + branch=self.branch_coverage(), + condition=self.condition_coverage(), + decision=self.decision_coverage(), + function=self.function_coverage(), + call=self.call_coverage(), + ) + + def function_coverage(self) -> CoverageStat: + """Return the function coverage statistic of the file.""" + total = 0 + covered = 0 + + for functioncov in self.functions.values(): + for lineno, excluded in functioncov.excluded.items(): + if not excluded: + total += 1 + if functioncov.count[lineno] > 0: + covered += 1 + + return CoverageStat(covered, total) + + def line_coverage(self) -> CoverageStat: + """Return the line coverage statistic of the file.""" + total = 0 + covered = 0 + + for linecov in self.lines.values(): + if linecov.is_reportable: + total += 1 + if linecov.is_covered: + covered += 1 + + return CoverageStat(covered, total) + + def branch_coverage(self) -> CoverageStat: + """Return the branch coverage statistic of the file.""" + stat = CoverageStat.new_empty() + + for linecov in self.lines.values(): + if linecov.is_reportable: + stat += linecov.branch_coverage() + + return stat + + def condition_coverage(self) -> CoverageStat: + """Return the condition coverage statistic of the file.""" + stat = CoverageStat.new_empty() + + for linecov in self.lines.values(): + if linecov.is_reportable: + stat += linecov.condition_coverage() + + return stat + + def decision_coverage(self) -> DecisionCoverageStat: + """Return the decision coverage statistic of the file.""" + stat = DecisionCoverageStat.new_empty() + + for linecov in self.lines.values(): + if linecov.is_reportable: + stat += linecov.decision_coverage() + + return stat + + def call_coverage(self) -> CoverageStat: + """Return the call coverage statistic of the file.""" + covered = 0 + total = 0 + + for linecov in self.lines.values(): + if linecov.is_reportable and len(linecov.calls) > 0: + for callcov in linecov.calls.values(): + if callcov.is_reportable: + total += 1 + if callcov.is_covered: + covered += 1 + + return CoverageStat(covered, total) + + +class CoverageContainer: + """Coverage container holding all the coverage data.""" + + def __init__(self) -> None: + self.data = dict[str, FileCoverage]() + self.directories = list[CoverageContainerDirectory]() + + def __getitem__(self, key: str) -> FileCoverage: + return self.data[key] + + def __len__(self) -> int: + return len(self.data) + + def __contains__(self, key: str) -> bool: + return key in self.data + + def __iter__(self) -> Iterator[str]: + return iter(self.data) + + def values(self) -> ValuesView[FileCoverage]: + """Get the file coverage data objects.""" + return self.data.values() + + def items(self) -> ItemsView[str, FileCoverage]: + """Get the file coverage data items.""" + return self.data.items() + + @property + def stats(self) -> SummarizedStats: + """Create a coverage statistic from a coverage data object.""" + stats = SummarizedStats.new_empty() + for filecov in self.values(): + stats += filecov.stats + return stats + + def sort_coverage( + self, + sort_key: Literal["filename", "uncovered-number", "uncovered-percent"], + sort_reverse: bool, + by_metric: Literal["line", "branch", "decision"], + filename_uses_relative_pathname: bool = False, + ) -> list[str]: + """Sort the coverage data""" + return sort_coverage( + self.data, + sort_key, + sort_reverse, + by_metric, + filename_uses_relative_pathname, + ) + + @staticmethod + def _get_dirname(filename: str) -> Optional[str]: + """Get the directory name with a trailing path separator. + + >>> import os + >>> CoverageContainer._get_dirname("bar/foobar.cpp".replace("/", os.sep)).replace(os.sep, "/") + 'bar/' + >>> CoverageContainer._get_dirname("/foo/bar/A/B.cpp".replace("/", os.sep)).replace(os.sep, "/") + '/foo/bar/A/' + >>> CoverageContainer._get_dirname(os.sep) is None + True + """ + if filename == os.sep: + return None + return str(os.path.dirname(filename.rstrip(os.sep))) + os.sep + + def populate_directories( + self, sorted_keys: Iterable[str], root_filter: re.Pattern[str] + ) -> None: + r"""Populate the list of directories and add accumulated stats. + + This function will accumulate statistics such that every directory + above it will know the statistics associated with all files deep within a + directory structure. + + Args: + sorted_keys: The sorted keys for covdata + root_filter: Information about the filter used with the root directory + """ + + # Get the directory coverage + subdirs = dict[str, CoverageContainerDirectory]() + for key in sorted_keys: + filecov = self[key] + dircov: Optional[CoverageContainerDirectory] = None + dirname: Optional[str] = ( + os.path.dirname(filecov.filename) + .replace("\\", os.sep) + .replace("/", os.sep) + .rstrip(os.sep) + ) + os.sep + while dirname is not None and root_filter.search(dirname + os.sep): + if dirname not in subdirs: + subdirs[dirname] = CoverageContainerDirectory(dirname) + if dircov is None: + subdirs[dirname][filecov.filename] = filecov + else: + subdirs[dirname].data[dircov.filename] = dircov + subdirs[dircov.filename].parent_dirname = dirname + subdirs[dirname].stats += filecov.stats + dircov = subdirs[dirname] + dirname = CoverageContainer._get_dirname(dirname) + + # Replace directories where only one sub container is available + # with the content this sub container + LOGGER.debug( + "Replace directories with only one sub element with the content of this." + ) + subdirs_to_remove = set() + for dirname, covdata_dir in subdirs.items(): + # There is exact one element, replace current element with referenced element + if len(covdata_dir) == 1: + # Get the orphan item + orphan_key, orphan_value = next(iter(covdata_dir.items())) + # The only child is a File object + if isinstance(orphan_value, FileCoverage): + # Replace the reference to ourself with our content + if covdata_dir.parent_dirname is not None: + LOGGER.debug( + f"Move {orphan_key} to {covdata_dir.parent_dirname}." + ) + parent_covdata_dir = subdirs[covdata_dir.parent_dirname] + parent_covdata_dir[orphan_key] = orphan_value + del parent_covdata_dir[dirname] + subdirs_to_remove.add(dirname) + else: + LOGGER.debug( + f"Move content of {orphan_value.dirname} to {dirname}." + ) + # Replace the children with the orphan ones + covdata_dir.data = orphan_value.data + # Change the parent key of each new child element + for new_child_value in covdata_dir.values(): + if isinstance(new_child_value, CoverageContainerDirectory): + new_child_value.parent_dirname = dirname + # Mark the key for removal. + subdirs_to_remove.add(orphan_key) + + for dirname in subdirs_to_remove: + del subdirs[dirname] + + self.directories = list(subdirs.values()) + + +class CoverageContainerDirectory: + """Represent coverage information about a directory.""" + + __slots__ = "dirname", "parent_dirname", "data", "stats" + + def __init__(self, dirname: str) -> None: + super().__init__() + self.dirname: str = dirname + self.parent_dirname: Optional[str] = None + self.data = dict[str, Union[FileCoverage, CoverageContainerDirectory]]() + self.stats: SummarizedStats = SummarizedStats.new_empty() + + def __setitem__( + self, key: str, item: Union[FileCoverage, CoverageContainerDirectory] + ) -> None: + self.data[key] = item + + def __getitem__(self, key: str) -> Union[FileCoverage, CoverageContainerDirectory]: + return self.data[key] + + def __delitem__(self, key: str) -> None: + del self.data[key] + + def __len__(self) -> int: + return len(self.data) + + def values(self) -> ValuesView[Union[FileCoverage, CoverageContainerDirectory]]: + """Get the file coverage data objects.""" + return self.data.values() + + def items(self) -> ItemsView[str, Union[FileCoverage, CoverageContainerDirectory]]: + """Get the file coverage data items.""" + return self.data.items() + + @property + def filename(self) -> str: + """Helpful function for when we use this DirectoryCoverage in a union with FileCoverage""" + return self.dirname + + def sort_coverage( + self, + sort_key: Literal["filename", "uncovered-number", "uncovered-percent"], + sort_reverse: bool, + by_metric: Literal["line", "branch", "decision"], + filename_uses_relative_pathname: bool = False, + ) -> list[str]: + """Sort the coverage data""" + return sort_coverage( + self.data, + sort_key, + sort_reverse, + by_metric, + filename_uses_relative_pathname, + ) + + def line_coverage(self) -> CoverageStat: + """A simple wrapper function necessary for sort_coverage().""" + return self.stats.line + + def branch_coverage(self) -> CoverageStat: + """A simple wrapper function necessary for sort_coverage().""" + return self.stats.branch + + def decision_coverage(self) -> DecisionCoverageStat: + """A simple wrapper function necessary for sort_coverage().""" + return self.stats.decision + + +@dataclass +class SummarizedStats: + """Data class for the summarized coverage statistics.""" + + line: CoverageStat + branch: CoverageStat + condition: CoverageStat + decision: DecisionCoverageStat + function: CoverageStat + call: CoverageStat + + @staticmethod + def new_empty() -> SummarizedStats: + """Create a empty coverage statistic.""" + return SummarizedStats( + line=CoverageStat.new_empty(), + branch=CoverageStat.new_empty(), + condition=CoverageStat.new_empty(), + decision=DecisionCoverageStat.new_empty(), + function=CoverageStat.new_empty(), + call=CoverageStat.new_empty(), + ) + + def __iadd__(self, other: SummarizedStats) -> SummarizedStats: + self.line += other.line + self.branch += other.branch + self.condition += other.condition + self.decision += other.decision + self.function += other.function + self.call += other.call + return self + + +@dataclass +class CoverageStat: + """A single coverage metric, e.g. the line coverage percentage of a file.""" + + covered: int + """How many elements were covered.""" + + total: int + """How many elements there were in total.""" + + @staticmethod + def new_empty() -> CoverageStat: + """Create a empty coverage statistic.""" + return CoverageStat(0, 0) + + @property + def percent(self) -> Optional[float]: + """Percentage of covered elements, equivalent to ``self.percent_or(None)``""" + return self.percent_or(None) + + def percent_or(self, default: _T) -> Union[float, _T]: + """Percentage of covered elements. + + Coverage is truncated to one decimal: + >>> CoverageStat(1234, 10000).percent_or("default") + 12.3 + + Coverage is capped at 99.9% unless everything is covered: + >>> CoverageStat(9999, 10000).percent_or("default") + 99.9 + >>> CoverageStat(10000, 10000).percent_or("default") + 100.0 + + If there are no elements, percentage is NaN and the default will be returned: + >>> CoverageStat(0, 0).percent_or("default") + 'default' + """ + if not self.total: + return default + + # Return 100% only if covered == total. + if self.covered == self.total: + return 100.0 + + # There is at least one uncovered item. + # Round to 1 decimal and clamp to max 99.9%. + ratio = self.covered / self.total + return min(99.9, round(ratio * 100.0, 1)) + + def __iadd__(self, other: CoverageStat) -> CoverageStat: + self.covered += other.covered + self.total += other.total + return self + + +@dataclass +class DecisionCoverageStat: + """A CoverageStat for decision coverage (accounts for Uncheckable cases).""" + + covered: int + uncheckable: int + total: int + + @classmethod + def new_empty(cls) -> DecisionCoverageStat: + """Create a empty decision coverage statistic.""" + return cls(0, 0, 0) + + @property + def to_coverage_stat(self) -> CoverageStat: + """Convert a decision coverage statistic to a coverage statistic.""" + return CoverageStat(covered=self.covered, total=self.total) + + @property + def percent(self) -> Optional[float]: + """Return the percent value of the coverage.""" + return self.to_coverage_stat.percent + + def percent_or(self, default: _T) -> Union[float, _T]: + """Return the percent value of the coverage or the given default if no coverage is present.""" + return self.to_coverage_stat.percent_or(default) + + def __iadd__(self, other: DecisionCoverageStat) -> DecisionCoverageStat: + self.covered += other.covered + self.uncheckable += other.uncheckable + self.total += other.total + return self diff --git a/scripts/reset_build.ps1 b/scripts/reset_build.ps1 new file mode 100644 index 0000000..975852d --- /dev/null +++ b/scripts/reset_build.ps1 @@ -0,0 +1,7 @@ +mv .\subprojects\packagecache . +rm -recurse -force .\subprojects\,.\builddir\ +mkdir subprojects +mv .\packagecache .\subprojects\ +mkdir builddir +cp wraps\*.wrap subprojects\ +meson setup --default-library=static --prefer-static builddir diff --git a/scripts/reset_build.sh b/scripts/reset_build.sh new file mode 100644 index 0000000..89931e7 --- /dev/null +++ b/scripts/reset_build.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +mv -f ./subprojects/packagecache . +rm -rf subprojects builddir +mkdir subprojects +mv -f packagecache ./subprojects/ && true +mkdir builddir +cp wraps/*.wrap subprojects/ +# on OSX you can't do this with static +meson setup --default-library=static --prefer-static builddir diff --git a/scripts/win_installer.ifp b/scripts/win_installer.ifp new file mode 100644 index 0000000..f1a9f29 Binary files /dev/null and b/scripts/win_installer.ifp differ diff --git a/tests/sample.cpp b/tests/sample.cpp new file mode 100644 index 0000000..6a10338 --- /dev/null +++ b/tests/sample.cpp @@ -0,0 +1,7 @@ +#include +#include +#include + +TEST_CASE("basic sample test", "[test-sample]") { + REQUIRE(1 + 2 == 3); +} diff --git a/wraps/catch2.wrap b/wraps/catch2.wrap new file mode 100644 index 0000000..f9bf436 --- /dev/null +++ b/wraps/catch2.wrap @@ -0,0 +1,11 @@ +[wrap-file] +directory = Catch2-3.7.1 +source_url = https://github.com/catchorg/Catch2/archive/v3.7.1.tar.gz +source_filename = Catch2-3.7.1.tar.gz +source_hash = c991b247a1a0d7bb9c39aa35faf0fe9e19764213f28ffba3109388e62ee0269c +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/catch2_3.7.1-1/Catch2-3.7.1.tar.gz +wrapdb_version = 3.7.1-1 + +[provide] +catch2 = catch2_dep +catch2-with-main = catch2_with_main_dep diff --git a/wraps/flac.wrap b/wraps/flac.wrap new file mode 100644 index 0000000..ee36479 --- /dev/null +++ b/wraps/flac.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = flac-1.4.3 +source_url = https://github.com/xiph/flac/releases/download/1.4.3/flac-1.4.3.tar.xz +source_filename = flac-1.4.3.tar.xz +source_hash = 6c58e69cd22348f441b861092b825e591d0b822e106de6eb0ee4d05d27205b70 +patch_filename = flac_1.4.3-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/flac_1.4.3-2/get_patch +patch_hash = 3eace1bd0769d3e0d4ff099960160766a5185d391c8f583293b087a1f96c2a9c +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/flac_1.4.3-2/flac-1.4.3.tar.xz +wrapdb_version = 1.4.3-2 + +[provide] +flac = flac_dep diff --git a/wraps/fmt.wrap b/wraps/fmt.wrap new file mode 100644 index 0000000..fd50847 --- /dev/null +++ b/wraps/fmt.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = fmt-11.0.2 +source_url = https://github.com/fmtlib/fmt/archive/11.0.2.tar.gz +source_filename = fmt-11.0.2.tar.gz +source_hash = 6cb1e6d37bdcb756dbbe59be438790db409cdb4868c66e888d5df9f13f7c027f +patch_filename = fmt_11.0.2-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/fmt_11.0.2-1/get_patch +patch_hash = 90c9e3b8e8f29713d40ca949f6f93ad115d78d7fb921064112bc6179e6427c5e +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/fmt_11.0.2-1/fmt-11.0.2.tar.gz +wrapdb_version = 11.0.2-1 + +[provide] +fmt = fmt_dep diff --git a/wraps/freetype2.wrap b/wraps/freetype2.wrap new file mode 100644 index 0000000..acad6f4 --- /dev/null +++ b/wraps/freetype2.wrap @@ -0,0 +1,11 @@ +[wrap-file] +directory = freetype-2.13.3 +source_url = https://download.savannah.gnu.org/releases/freetype/freetype-2.13.3.tar.xz +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/freetype2_2.13.3-1/freetype-2.13.3.tar.xz +source_filename = freetype-2.13.3.tar.xz +source_hash = 0550350666d427c74daeb85d5ac7bb353acba5f76956395995311a9c6f063289 +wrapdb_version = 2.13.3-1 + +[provide] +freetype2 = freetype_dep +freetype = freetype_dep diff --git a/wraps/lel-guecs.wrap b/wraps/lel-guecs.wrap new file mode 100644 index 0000000..1e3bbda --- /dev/null +++ b/wraps/lel-guecs.wrap @@ -0,0 +1,10 @@ +[wrap-git] +directory=lel-guecs-0.2.0 +url=https://git.learnjsthehardway.com/learn-code-the-hard-way/lel-guecs.git +revision=HEAD +depth=1 +method=meson + +[provide] +lel_guecs = lel_guecs_dep +lel_guecs_sfml = lel_guecs_sfml_dep diff --git a/wraps/libpng.wrap b/wraps/libpng.wrap new file mode 100644 index 0000000..06044a9 --- /dev/null +++ b/wraps/libpng.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = libpng-1.6.44 +source_url = https://github.com/glennrp/libpng/archive/v1.6.44.tar.gz +source_filename = libpng-1.6.44.tar.gz +source_hash = 0ef5b633d0c65f780c4fced27ff832998e71478c13b45dfb6e94f23a82f64f7c +patch_filename = libpng_1.6.44-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/libpng_1.6.44-1/get_patch +patch_hash = 394b07614c45fbd1beac8b660386216a490fe12f841a1a445799b676c9c892fb +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/libpng_1.6.44-1/libpng-1.6.44.tar.gz +wrapdb_version = 1.6.44-1 + +[provide] +libpng = libpng_dep diff --git a/wraps/nlohmann_json.wrap b/wraps/nlohmann_json.wrap new file mode 100644 index 0000000..8c46676 --- /dev/null +++ b/wraps/nlohmann_json.wrap @@ -0,0 +1,11 @@ +[wrap-file] +directory = nlohmann_json-3.11.3 +lead_directory_missing = true +source_url = https://github.com/nlohmann/json/releases/download/v3.11.3/include.zip +source_filename = nlohmann_json-3.11.3.zip +source_hash = a22461d13119ac5c78f205d3df1db13403e58ce1bb1794edc9313677313f4a9d +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/nlohmann_json_3.11.3-1/nlohmann_json-3.11.3.zip +wrapdb_version = 3.11.3-1 + +[provide] +nlohmann_json = nlohmann_json_dep diff --git a/wraps/ogg.wrap b/wraps/ogg.wrap new file mode 100644 index 0000000..e7f23eb --- /dev/null +++ b/wraps/ogg.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = libogg-1.3.5 +source_url = https://downloads.xiph.org/releases/ogg/libogg-1.3.5.tar.xz +source_filename = libogg-1.3.5.tar.xz +source_hash = c4d91be36fc8e54deae7575241e03f4211eb102afb3fc0775fbbc1b740016705 +patch_filename = ogg_1.3.5-6_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/ogg_1.3.5-6/get_patch +patch_hash = 8be6dcd5f93bbf9c0b9c8ec1fa29810226a60f846383074ca05b313a248e78b2 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/ogg_1.3.5-6/libogg-1.3.5.tar.xz +wrapdb_version = 1.3.5-6 + +[provide] +ogg = libogg_dep diff --git a/wraps/sfml.wrap b/wraps/sfml.wrap new file mode 100644 index 0000000..577ed1e --- /dev/null +++ b/wraps/sfml.wrap @@ -0,0 +1,14 @@ +[wrap-git] +directory=SFML-3.0.0 +url=https://github.com/SFML/SFML.git +revision=3.0.0 +depth=1 +method=cmake + +[provide] +sfml_audio = sfml_audio_dep +sfml_graphics = sfml_graphics_dep +sfml_main = sfml_main_dep +sfml_network = sfml_network_dep +sfml_system = sfml_system_dep +sfml_window = sfml_window_dep diff --git a/wraps/tracy.wrap b/wraps/tracy.wrap new file mode 100644 index 0000000..3e1bda5 --- /dev/null +++ b/wraps/tracy.wrap @@ -0,0 +1,7 @@ +[wrap-git] +url=https://github.com/wolfpld/tracy.git +revision=v0.11.1 +depth=1 + +[provide] +tracy = tracy_dep diff --git a/wraps/vorbis.wrap b/wraps/vorbis.wrap new file mode 100644 index 0000000..7425c11 --- /dev/null +++ b/wraps/vorbis.wrap @@ -0,0 +1,14 @@ +[wrap-file] +directory = libvorbis-1.3.7 +source_url = https://downloads.xiph.org/releases/vorbis/libvorbis-1.3.7.tar.xz +source_filename = libvorbis-1.3.7.tar.xz +source_hash = b33cc4934322bcbf6efcbacf49e3ca01aadbea4114ec9589d1b1e9d20f72954b +patch_filename = vorbis_1.3.7-4_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/vorbis_1.3.7-4/get_patch +patch_hash = 979e22b24b16c927040700dfd8319cd6ba29bf52a14dbc66b1cb4ea60504f14a +wrapdb_version = 1.3.7-4 + +[provide] +vorbis = vorbis_dep +vorbisfile = vorbisfile_dep +vorbisenc = vorbisenc_dep