diff --git a/.gitignore b/.gitignore index 55a4eb0e31b..86a01d53594 100644 --- a/.gitignore +++ b/.gitignore @@ -195,3 +195,4 @@ libs/openFrameworksCompiled/project/android/build-*/ examples/utils/urlFileLoaderExample/bin/data +tests/graphics/imageFormats/bin/data/* diff --git a/scripts/of.sh b/scripts/of.sh index 55af5037781..de52088c6db 100755 --- a/scripts/of.sh +++ b/scripts/of.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # of.sh - openFrameworks CLI | Dan Rosser 2025 -OF_SCRIPT_VERSION=0.4.3 +OF_SCRIPT_VERSION=0.4.4 OF_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" OF_DIR="$(realpath "$OF_DIR/../")" @@ -1051,14 +1051,25 @@ menuTest(){ printBanner "test" local -a opts=("All tests/ smoke tests (build + run)|tests-all") - local g gName + local -a testOpts=() + local -a groupOpts=() + local g gName test testName if [[ -d "${OF_DIR}/tests" ]]; then for g in "${OF_DIR}/tests"/*/; do [[ -d "$g" ]] || continue gName=$(basename "${g%/}") - opts+=("tests/${gName} only|tests-${gName}") + groupOpts+=("All tests/${gName}|tests-${gName}") + for test in "$g"*/; do + [[ -d "${test}src" ]] || continue + testName=$(basename "${test%/}") + testOpts+=("${gName}/${testName}|test:${gName}/${testName}") + done done fi + # Keep every directly runnable project together at the top of the menu; + # broader group runs follow them. + opts+=("${testOpts[@]}") + opts+=("${groupOpts[@]}") local -a smokeScripts=() local s for s in "${OF_CORE_SCRIPT_DIR}/dev"/smoke_test_*.sh; do @@ -1074,6 +1085,7 @@ menuTest(){ case "$choice" in tests-all) cmdTest ;; tests-*) cmdTest "${choice#tests-}" ;; + test:*) cmdTest "${choice#test:}" ;; script:*) local scriptName="${choice#script:}" local scriptPath="${OF_CORE_SCRIPT_DIR}/dev/${scriptName}" @@ -1560,9 +1572,52 @@ menuPickApoVsVer(){ return 0 } +menuPickApoArch(){ + local type="$1" a + local -a choices=() arches=() + if [[ "$type" == "vs" ]]; then + choices=( + "All (x64 + ARM64 + ARM64EC)|all" + "x64 (64)|64" + "ARM64|arm64" + "ARM64EC|arm64ec" + "ARM64 + ARM64EC|arm64+arm64ec" + ) + else + read -r -a arches <<< "$(archesForApoType "$type")" + for a in "${arches[@]}"; do choices+=("${a}|${a}"); done + fi + menuPick "Target architecture · ${type}" "${choices[@]}" +} + +# Keep the native platform first in Apothecary's platform menus. In +# particular, Windows users should see Visual Studio before Apple targets. +apoBuildTypesOrdered(){ + local type + if [[ "$OF_PLATFORM" == "vs" ]]; then + printf '%s\n' vs + fi + for type in "${APO_BUILD_TYPES[@]}"; do + [[ "$OF_PLATFORM" == "vs" && "$type" == "vs" ]] && continue + printf '%s\n' "$type" + done +} + runApothecaryEngine(){ local type="$1" arch="$2" shift 2 + # Apothecary accepts one VS architecture per invocation. Let the oF menu + # expose useful groups while keeping the underlying calls conventional. + if [[ "$type" == "vs" && ( "$arch" == "all" || "$arch" == *","* || "$arch" == *"+"* ) ]]; then + local expanded="$arch" vsArch + [[ "$expanded" == "all" ]] && expanded="64,arm64,arm64ec" + expanded="${expanded//+/,}" + for vsArch in ${expanded//,/ }; do + echoInfo "Visual Studio libraries · ${vsArch}" + runApothecaryEngine "$type" "$vsArch" "$@" || return $? + done + return 0 + fi local -a cmd=() resolveApothecary [[ -f "$APO_ENGINE" ]] || { echoError "apothecary engine missing — git submodule update --init scripts/apothecary"; return 1; } @@ -1658,7 +1713,7 @@ menuApothecary(){ ;; build-type) opts=() - for type in "${APO_BUILD_TYPES[@]}"; do + while IFS= read -r type; do if [[ "$type" == "macos" ]]; then opts+=("macos — Apple multi-target xcframeworks|macos") elif [[ "$type" == "osx" ]]; then @@ -1666,7 +1721,7 @@ menuApothecary(){ else opts+=("${type}|${type}") fi - done + done < <(apoBuildTypesOrdered) [[ "$OF_PLATFORM" == "osx" ]] && echoNote "macos covers osx · ios · tvos · xros · watchos · catos" menuPick "Build platform" "${opts[@]}" || return 2 type="$UI_MENU_RESULT" @@ -1674,29 +1729,21 @@ menuApothecary(){ menuPickApoVsVer || return 2 export OF_APO_VS_VER="$UI_MENU_RESULT" fi - aopts=() - read -r -a arches <<< "$(archesForApoType "$type")" - for a in "${arches[@]}"; do - aopts+=("${a}|${a}") - done - menuPick "Architecture · ${type}" "${aopts[@]}" || return 2 + menuPickApoArch "$type" || return 2 cmdApothecaryBuildAll "$type" "$UI_MENU_RESULT" ;; build-one) opts=() - for type in "${APO_BUILD_TYPES[@]}"; do + while IFS= read -r type; do opts+=("${type}|${type}") - done + done < <(apoBuildTypesOrdered) menuPick "Platform" "${opts[@]}" || return 2 type="$UI_MENU_RESULT" if [[ "$type" == "vs" ]]; then menuPickApoVsVer || return 2 export OF_APO_VS_VER="$UI_MENU_RESULT" fi - aopts=() - read -r -a arches <<< "$(archesForApoType "$type")" - for a in "${arches[@]}"; do aopts+=("${a}|${a}"); done - menuPick "Architecture · ${type}" "${aopts[@]}" || return 2 + menuPickApoArch "$type" || return 2 arch="$UI_MENU_RESULT" opts=() local f @@ -2542,6 +2589,13 @@ cmdBuild(){ ;; esac ;; + tests|test) + if [[ "$OF_PLATFORM" == "vs" ]]; then + runHostTests "${1:-Debug}" "${2:-${OF_VS_PLATFORM:-$(vsPlatformFromArch)}}" "${3:-${OF_VS_TOOLSET:-$(vsToolsetDefault)}}" + else + runHostTests "$@" + fi + ;; clean) local path="${1:-}" [[ -n "$path" ]] || { echoError "usage: of build clean "; return 1; } diff --git a/scripts/of_build.sh b/scripts/of_build.sh index f698ef83880..c6921487296 100644 --- a/scripts/of_build.sh +++ b/scripts/of_build.sh @@ -484,6 +484,75 @@ runMsbuildProject(){ -nologo -m } +# Regenerate every unit-test project for Visual Studio, then compile it with +# the same platform/toolset selection used by normal project builds. Tests +# are compile-only here: ARM64/ARM64EC binaries may be cross-built on x64. +runVsTests(){ + local config="${1:-Debug}" + local platform="${2:-${OF_VS_PLATFORM:-$(vsPlatformFromArch)}}" + local toolset="${3:-${OF_VS_TOOLSET:-$(vsToolsetDefault)}}" + local pgTemplate="${4:-${OF_PG_TEMPLATE:-vs2026}}" + local pg msbuild project count=0 failed=0 + + pg=$(findPGBinary) || { echoError "Project Generator missing — run: of update pg"; return 1; } + msbuild=$(findMsbuildBinary) || { echoError "msbuild.exe not found"; return 1; } + printBanner "tests" + echoKV "target" "${platform} · ${config} · ${toolset}" + echoInfo "regenerating Visual Studio projects under tests/" + local -a pgArgs=( -r -o"$OF_DIR" -pvs ) + [[ -n "$pgTemplate" ]] && pgArgs+=( -t"$pgTemplate" ) + PG_OF_PATH="$OF_DIR" "$pg" "${pgArgs[@]}" "${OF_DIR}/tests" || return $? + + while IFS= read -r project; do + count=$((count + 1)) + echoInfo "test ${count} · ${project#${OF_DIR}/}" + "$msbuild" "$project" -nologo -m \ + "-p:Configuration=${config}" \ + "-p:Platform=${platform}" \ + "-p:PlatformToolset=${toolset}" || failed=$((failed + 1)) + done < <(find "${OF_DIR}/tests" -mindepth 2 -name '*.vcxproj' -type f | sort) + [[ $count -gt 0 ]] || { echoError "no generated test projects found"; return 1; } + [[ $failed -eq 0 ]] || { echoError "${failed}/${count} test projects failed to compile"; return 1; } + echoSuccess "${count} test projects compiled · ${platform}/${config}" +} + +menuVsTests(){ + local config toolset platform template + menuPickConfig || return 0; config="$UI_MENU_RESULT" + menuPickVsToolset || return 0; toolset="$UI_MENU_RESULT" + menuPickVsPlatform || return 0; platform="$UI_MENU_RESULT" + menuPickPgTemplate || return 0; template="$UI_MENU_RESULT" + runVsTests "$config" "$platform" "$toolset" "$template" +} + +runHostTests(){ + case "$OF_PLATFORM" in + vs) + runVsTests "$@" + ;; + osx|macos) + local runner="${OF_DIR}/scripts/ci/${OF_PLATFORM}/run_tests.sh" + [[ -f "$runner" ]] || runner="${OF_DIR}/scripts/ci/osx/run_tests.sh" + echoInfo "macOS tests · make Debug + RunDebug" + bash "$runner" + ;; + *) + echoError "tests() is currently available for macOS and Visual Studio" + return 1 + ;; + esac +} + +menuHostTests(){ + case "$OF_PLATFORM" in + vs) menuVsTests ;; + osx|macos) + confirmYes "Build and run the macOS unit tests?" || return 0 + runHostTests + ;; + esac +} + # --------------------------------------------------------------------------- # Project file helpers # --------------------------------------------------------------------------- @@ -923,18 +992,25 @@ runTestProject(){ # reusable from the CLI/menu. Prints a tick per test, full output only on # failure, and a pass/fail summary at the end. cmdTest(){ - local filterGroup="${1:-}" + local filter="${1:-}" + local filterGroup="${filter%%/*}" + local filterTest="" + [[ "$filter" == */* ]] && filterTest="${filter#*/}" local testsDir="${OF_DIR}/tests" [[ -d "$testsDir" ]] || { echoError "no tests/ directory found"; return 1; } printBanner "test" - echoInfo "running smoke tests under tests/${filterGroup:+$filterGroup/}" + echoInfo "running smoke tests under tests/${filter:+$filter/}" printf '\n' local -a groups=() local g if [[ -n "$filterGroup" ]]; then [[ -d "${testsDir}/${filterGroup}" ]] || { echoError "no tests/${filterGroup} folder"; return 1; } + if [[ -n "$filterTest" && ! -d "${testsDir}/${filterGroup}/${filterTest}/src" ]]; then + echoError "no tests/${filterGroup}/${filterTest} project" + return 1 + fi groups=("${testsDir}/${filterGroup}") else for g in "${testsDir}"/*/; do @@ -953,6 +1029,7 @@ cmdTest(){ [[ -d "$test" && -d "${test}src" ]] || continue test="${test%/}" name=$(basename "$test") + [[ -n "$filterTest" && "$name" != "$filterTest" ]] && continue total=$((total + 1)) # \r-overwrite the pending marker only when actually attached to a # terminal -- otherwise (piped/logged output) it just garbles two @@ -1230,7 +1307,7 @@ menuBuild(){ echoKV "cmake" "$(command -v cmake >/dev/null 2>&1 && cmake --version 2>/dev/null | head -1 || echo 'not installed')" printf '\n' - menuPick "Build…" \ + local -a buildOpts=( "OF core library (make)|core" \ "Project under apps/…|apps" \ "Example under examples/…|examples" \ @@ -1240,15 +1317,26 @@ menuBuild(){ "Generate project files (PG)…|generate" \ "CMake…|cmake" \ "Clean project…|clean" \ - "Apothecary — build libraries…|apothecary" \ + "Apothecary — build libraries…|apothecary" + ) + case "$OF_PLATFORM" in + vs) buildOpts+=("Tests — PG + MSBuild…|tests") ;; + osx|macos) buildOpts+=("Tests — make + run…|tests") ;; + esac + buildOpts+=( \ "Upgrade — Projects / Addons…|upgrade" \ "Help|help" \ - "Back|back" \ - || return 0 + "Back|back" + ) + menuPick "Build…" "${buildOpts[@]}" || return 0 choice="$UI_MENU_RESULT" printf '\n' case "$choice" in + tests) + menuHostTests + menuPause + ;; core) menuPickConfig || continue cmdBuildCore "$UI_MENU_RESULT" @@ -1410,6 +1498,9 @@ printHelpBuild(){ ${prog} build generate [platforms] ${prog} build cmake [Debug|Release] ${prog} build cmake status + ${prog} build tests macOS: make + run all unit tests + ${prog} build tests [cfg] [platform] [toolset] + Windows: PG + MSBuild (compile) ${prog} build clean ${prog} build open-em Open emscripten index.html (Chrome/emrun) diff --git a/tests/graphics/imageFormats/addons.make b/tests/graphics/imageFormats/addons.make new file mode 100644 index 00000000000..0e0303d2e09 --- /dev/null +++ b/tests/graphics/imageFormats/addons.make @@ -0,0 +1 @@ +ofxUnitTests diff --git a/tests/graphics/imageFormats/bin/data/.gitkeep b/tests/graphics/imageFormats/bin/data/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/graphics/imageFormats/icon.rc b/tests/graphics/imageFormats/icon.rc new file mode 100644 index 00000000000..7e26eb3534f --- /dev/null +++ b/tests/graphics/imageFormats/icon.rc @@ -0,0 +1,8 @@ +// Icon Resource Definition +#define MAIN_ICON 102 + +#if defined(_DEBUG) +MAIN_ICON ICON "icon_debug.ico" +#else +MAIN_ICON ICON "icon.ico" +#endif diff --git a/tests/graphics/imageFormats/src/main.cpp b/tests/graphics/imageFormats/src/main.cpp new file mode 100644 index 00000000000..65b707f6aad --- /dev/null +++ b/tests/graphics/imageFormats/src/main.cpp @@ -0,0 +1,97 @@ +#include "ofMain.h" +#include "ofAppNoWindow.h" +#include "ofxUnitTests.h" + +// Round-trips a synthetic test pattern through FreeImage (via ofImage) for a +// handful of formats and checks the reloaded pixels actually match the +// pattern, not just that width/height came back right. +class ofApp: public ofxUnitTestsApp{ + + static const int kBlockSize = 8; + static const int kNumColors = 8; + + // small, fixed palette so the pattern survives GIF's 256-color quantize + ofColor paletteColor(int i){ + static const ofColor palette[kNumColors] = { + ofColor(230, 60, 60), ofColor(60, 230, 60), ofColor(60, 60, 230), ofColor(230, 230, 60), + ofColor(230, 60, 230), ofColor(60, 230, 230), ofColor(250, 250, 250), ofColor(20, 20, 20) + }; + return palette[i % kNumColors]; + } + + ofColor patternColor(int x, int y){ + int block = (x / kBlockSize) + (y / kBlockSize); + return paletteColor(block); + } + + ofImage makeTestImage(int w, int h){ + ofImage img; + img.setUseTexture(false); + img.allocate(w, h, OF_IMAGE_COLOR); + for(int y = 0; y < h; y++){ + for(int x = 0; x < w; x++){ + img.setColor(x, y, patternColor(x, y)); + } + } + return img; + } + + bool colorsClose(const ofColor & a, const ofColor & b, int tolerance){ + return abs((int)a.r - (int)b.r) <= tolerance + && abs((int)a.g - (int)b.g) <= tolerance + && abs((int)a.b - (int)b.b) <= tolerance; + } + + void testFormat(const string & ext, int tolerance){ + const int w = 64, h = 48; + ofImage img = makeTestImage(w, h); + + string path = "test_pattern." + ext; + if(!ofxTest(img.save(path), "save " + ext)){ + return; + } + + ofImage loaded; + loaded.setUseTexture(false); + if(!ofxTest(loaded.load(path), "load " + ext)){ + return; + } + + ofxTestEq((int)loaded.getWidth(), w, "width " + ext); + ofxTestEq((int)loaded.getHeight(), h, "height " + ext); + + // sample block centers (avoids compression artifacts at block edges) + bool pixelsOk = true; + for(int by = kBlockSize / 2; by < h; by += kBlockSize){ + for(int bx = kBlockSize / 2; bx < w; bx += kBlockSize){ + ofColor expected = patternColor(bx, by); + ofColor actual = loaded.getColor(bx, by); + if(!colorsClose(expected, actual, tolerance)){ + pixelsOk = false; + ofLogError() << ext << " pixel(" << bx << "," << by << ") expected " + << (int)expected.r << "," << (int)expected.g << "," << (int)expected.b + << " got " << (int)actual.r << "," << (int)actual.g << "," << (int)actual.b; + } + } + } + ofxTest(pixelsOk, "pixel content matches pattern " + ext); + } + + void run(){ + testFormat("png", 0); // lossless + testFormat("bmp", 0); // lossless + testFormat("tif", 0); // lossless + testFormat("tga", 0); // lossless + testFormat("gif", 8); // 256-color quantize, small slack + testFormat("jpg", 24); // lossy DCT, sampled at block centers + } +}; + +//======================================================================== +int main( ){ + ofInit(); + auto window = std::make_shared(); + auto app = std::make_shared(); + ofRunApp(window, app); + return ofRunMainLoop(); +}