Add a simple harness for automated, headless testing.

This commit alters the build system substantially; it adds another
platform, `headless`, that provides stubs in place of all GUI
functions, and provides a library `solvespace_headless` alongside
the main executable. To cut down build times, only the few files
that have #if defined(HEADLESS) are built twice for the executable
and the library; the rest is grouped into a new `solvespace_cad`
library. It is not usable on its own and just serves for grouping.

This commit also gates the tests behind a -DENABLE_TESTS=ON CMake
option, ON by default (but suggested as OFF in the README so that
people don't ever have to install cairo to build the executable.)

The tests introduced in this commit are (so far) rudimentary,
although functional, and they serve as a stepping point towards
introducing coverage analysis.
pull/33/head
whitequark 2016-07-25 19:37:48 +00:00
parent 977a0b8e6d
commit 5e63d8301e
28 changed files with 1482 additions and 130 deletions

14
.gitattributes vendored
View File

@ -1,22 +1,10 @@
# .gitattributes for SolveSpace
# Set default behaviour, in case users don't have core.autocrlf set.
* text=auto
# Explicitly declare text files we want to always be normalized and converted
# to native line endings on checkout.
*.cpp text
*.h text
*.txt text
# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.gz binary
*.ico binary
*.jpg binary
*.lib binary
*.png binary
# end .gitattributes
*.slvs binary

2
.gitignore vendored
View File

@ -1,5 +1,7 @@
/CMakeCache.txt
/build*/
/test/**/*.diff.*
/test/**/*.curr.*
*.trace # OpenGL apitrace files
/debian/tmp/
/debian/*.log

View File

@ -2,5 +2,7 @@
if echo $TRAVIS_TAG | grep ^v; then BUILD_TYPE=RelWithDebInfo; else BUILD_TYPE=Debug; fi
export BUILD_TYPE
dpkg-buildpackage -b -us -uc
mkdir build
cd build
cmake -DCMAKE_C_COMPILER=gcc-5 -DCMAKE_CXX_COMPILER=g++-5 -DCMAKE_BUILD_TYPE=$BUILD_TYPE ..
make VERBOSE=1

View File

@ -27,6 +27,8 @@ set(solvespace_VERSION_MAJOR 3)
set(solvespace_VERSION_MINOR 0)
string(SUBSTRING "${GIT_COMMIT_HASH}" 0 8 solvespace_GIT_HASH)
set(ENABLE_TESTS ON CACHE BOOL "Whether the test suite will be built and run")
if(NOT WIN32 AND NOT APPLE)
set(GUI gtk2 CACHE STRING "GUI toolkit to use (one of: gtk2 gtk3)")
endif()
@ -93,6 +95,7 @@ if(WIN32)
PNG_PNG_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/extlib/libpng)
list(APPEND PNG_PNG_INCLUDE_DIR ${CMAKE_BINARY_DIR}/extlib/libpng)
if(ENABLE_TESTS)
message(STATUS "Using in-tree pixman")
add_vendored_subdirectory(extlib/pixman)
set(PIXMAN_FOUND YES)
@ -103,9 +106,10 @@ if(WIN32)
message(STATUS "Using in-tree cairo")
add_vendored_subdirectory(extlib/cairo)
set(CAIRO_FOUND YES)
set(CAIRO_LIBRARY cairo)
set(CAIRO_LIBRARIES cairo)
set(CAIRO_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/cairo/src)
list(APPEND CAIRO_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/cairo/src)
endif()
if(NOT MINGW)
message(STATUS "Using prebuilt SpaceWare")
@ -122,10 +126,15 @@ elseif(APPLE)
find_package(PNG REQUIRED)
find_package(Freetype REQUIRED)
find_library(APPKIT_LIBRARY AppKit REQUIRED)
find_library(CAIRO_LIBRARY cairo REQUIRED)
if(ENABLE_TESTS)
find_library(CAIRO_LIBRARIES cairo REQUIRED)
find_path(CAIRO_INCLUDE_DIRS cairo.h PATH_SUFFIXES cairo)
endif()
find_library(APPKIT_LIBRARY AppKit REQUIRED)
else() # Linux and compatible systems
find_package(PkgConfig REQUIRED)
find_package(Backtrace)
find_package(SpaceWare)
@ -133,12 +142,13 @@ else() # Linux and compatible systems
find_package(PNG REQUIRED)
find_package(Freetype REQUIRED)
# Use freedesktop's pkg-config to locate everything else.
find_package(PkgConfig REQUIRED)
if(ENABLE_TESTS)
pkg_check_modules(CAIRO REQUIRED cairo)
endif()
pkg_check_modules(FONTCONFIG REQUIRED fontconfig)
pkg_check_modules(JSONC REQUIRED json-c)
pkg_check_modules(FREETYPE REQUIRED freetype2)
pkg_check_modules(CAIRO REQUIRED cairo)
set(HAVE_GTK TRUE)
if(GUI STREQUAL "gtk3")
@ -192,15 +202,18 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang
set(WARNING_FLAGS "${WARNING_FLAGS} -Werror=switch")
endif()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WARNING_FLAGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WARNING_FLAGS}")
if(WIN32)
set(CMAKE_RC_FLAGS "${CMAKE_RC_FLAGS} -l0")
endif()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WARNING_FLAGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WARNING_FLAGS}")
# components
add_subdirectory(res)
add_subdirectory(src)
add_subdirectory(exposed)
if(ENABLE_TESTS)
add_subdirectory(test)
endif()

View File

@ -26,12 +26,12 @@ Building on Linux
### Building for Linux
You will need CMake, libpng, zlib, json-c, fontconfig, freetype, gtkmm 2.4,
pangomm 1.4, OpenGL and OpenGL GLU.
pangomm 1.4, OpenGL and OpenGL GLU. To build tests, you will need cairo.
On a Debian derivative (e.g. Ubuntu) these can be installed with:
apt-get install libpng-dev libjson-c-dev libfreetype6-dev \
libfontconfig1-dev libgtkmm-2.4-dev libpangomm-1.4-dev \
libgl-dev libglu-dev cmake
libcairo2-dev libgl-dev libglu-dev cmake
Before building, check out the necessary submodules:
@ -41,7 +41,7 @@ After that, build SolveSpace as following:
mkdir build
cd build
cmake ..
cmake .. -DENABLE_TESTS=OFF
make
sudo make install
@ -63,15 +63,17 @@ After that, build 32-bit SolveSpace as following:
mkdir build
cd build
cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw32.cmake ..
make solvespace
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw32.cmake \
-DENABLE_TESTS=OFF
make
Or, build 64-bit SolveSpace as following:
mkdir build
cd build
cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw64.cmake ..
make solvespace
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw64.cmake \
-DENABLE_TESTS=OFF
make
The application is built as `build/src/solvespace.exe`.
@ -80,10 +82,11 @@ Space Navigator support will not be available.
Building on Mac OS X
--------------------
You will need XCode tools, CMake, libpng and Freetype. Assuming you use
You will need XCode tools, CMake, libpng and Freetype. To build tests, you
will need cairo. Assuming you use
[homebrew][], these can be installed with:
brew install cmake libpng freetype
brew install cmake libpng freetype cairo
XCode has to be installed via AppStore; it requires a free Apple ID.
@ -95,7 +98,7 @@ After that, build SolveSpace as following:
mkdir build
cd build
cmake ..
cmake .. -DENABLE_TESTS=OFF
make
The app bundle is built in `build/src/solvespace.app`.
@ -123,7 +126,7 @@ Visual Studio install. Then, run the following in cmd or PowerShell:
git submodule update --init
mkdir build
cd build
cmake .. -G "NMake Makefiles"
cmake .. -G "NMake Makefiles" -DENABLE_TESTS=OFF
nmake
### MSVC build
@ -137,7 +140,7 @@ in bash:
git submodule update --init
mkdir build
cd build
cmake ..
cmake .. -DENABLE_TESTS=OFF
make
[cmakewin]: http://www.cmake.org/download/#latest

View File

@ -6,9 +6,12 @@ before_build:
- cd build
- set tag=x%APPVEYOR_REPO_TAG_NAME%
- if %tag:~,2% == xv (set BUILD_TYPE=RelWithDebInfo) else (set BUILD_TYPE=Debug)
- cmake -G"Visual Studio 12" -T v120_xp -DCMAKE_BUILD_TYPE=%BUILD_TYPE% ..
- cmake -G"Visual Studio 12" -T v120_xp ..
build_script:
- msbuild "src\solvespace.vcxproj" /verbosity:minimal /property:Configuration=%BUILD_TYPE% /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
- msbuild "test\solvespace_testsuite.vcxproj" /verbosity:minimal /property:Configuration=%BUILD_TYPE% /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
test_script:
- test\%BUILD_TYPE%\solvespace_testsuite.exe
artifacts:
- path: build\src\Debug\solvespace.exe
name: solvespace.exe

View File

@ -72,7 +72,8 @@ endif()
if(WIN32)
set(platform_SOURCES
platform/w32main.cpp)
platform/w32main.cpp
render/rendergl1.cpp)
set(platform_LIBRARIES
comctl32
@ -83,7 +84,8 @@ elseif(APPLE)
set(platform_SOURCES
platform/cocoamain.mm
render/rendergl.cpp)
render/rendergl.cpp
render/rendergl1.cpp)
set(platform_BUNDLED_LIBS
${PNG_LIBRARIES}
@ -94,10 +96,10 @@ elseif(APPLE)
elseif(HAVE_GTK)
set(platform_SOURCES
platform/gtkmain.cpp
render/rendergl.cpp)
render/rendergl.cpp
render/rendergl1.cpp)
set(platform_LIBRARIES
${Backtrace_LIBRARIES}
${SPACEWARE_LIBRARIES})
foreach(pkg_config_lib GTKMM JSONC FONTCONFIG)
@ -107,9 +109,9 @@ elseif(HAVE_GTK)
endforeach()
endif()
# solvespace executable
# solvespace library
set(solvespace_HEADERS
set(solvespace_cad_HEADERS
config.h
dsc.h
expr.h
@ -120,18 +122,15 @@ set(solvespace_HEADERS
render/render.h
srf/surface.h)
set(solvespace_SOURCES
set(solvespace_cad_SOURCES
bsp.cpp
clipboard.cpp
confscreen.cpp
constraint.cpp
constrainteq.cpp
describescreen.cpp
draw.cpp
drawconstraint.cpp
drawentity.cpp
entity.cpp
export.cpp
exportstep.cpp
exportvector.cpp
expr.cpp
@ -147,20 +146,16 @@ set(solvespace_SOURCES
polygon.cpp
resource.cpp
request.cpp
solvespace.cpp
style.cpp
system.cpp
textscreens.cpp
textwin.cpp
toolbar.cpp
ttf.cpp
undoredo.cpp
util.cpp
view.cpp
render/render.cpp
render/rendergl1.cpp
render/render2d.cpp
render/rendercairo.cpp
srf/boolean.cpp
srf/curve.cpp
srf/merge.cpp
@ -170,25 +165,38 @@ set(solvespace_SOURCES
srf/surfinter.cpp
srf/triangulate.cpp)
add_executable(solvespace WIN32 MACOSX_BUNDLE
${libslvs_HEADERS}
${libslvs_SOURCES}
set(solvespace_cad_gl_SOURCES
confscreen.cpp
draw.cpp
export.cpp
solvespace.cpp
textwin.cpp)
add_library(solvespace_cad STATIC
${util_SOURCES}
${solvespace_cad_HEADERS}
${solvespace_cad_SOURCES})
target_link_libraries(solvespace_cad
dxfrw
${ZLIB_LIBRARY}
${PNG_LIBRARY}
${FREETYPE_LIBRARY}
${Backtrace_LIBRARIES})
# solvespace gui executable
add_executable(solvespace WIN32 MACOSX_BUNDLE
${solvespace_cad_gl_SOURCES}
${platform_SOURCES}
${solvespace_HEADERS}
${solvespace_SOURCES}
$<TARGET_PROPERTY:resources,EXTRA_SOURCES>)
add_dependencies(solvespace
resources)
target_link_libraries(solvespace
dxfrw
solvespace_cad
${OPENGL_LIBRARIES}
${ZLIB_LIBRARY}
${PNG_LIBRARY}
${FREETYPE_LIBRARY}
${CAIRO_LIBRARY}
${platform_LIBRARIES})
if(WIN32 AND NOT MINGW)
@ -229,21 +237,22 @@ if(NOT WIN32)
BUNDLE DESTINATION .)
endif()
# valgrind
# solvespace headless library
add_custom_target(solvespace-valgrind
valgrind
--tool=memcheck
--verbose
--track-fds=yes
--log-file=vg.%p.out
--num-callers=50
--error-limit=no
--read-var-info=yes
--leak-check=full
--leak-resolution=high
--show-reachable=yes
--track-origins=yes
--malloc-fill=0xac
--free-fill=0xde
$<TARGET_FILE:solvespace>)
set(headless_SOURCES
platform/headless.cpp
render/rendercairo.cpp)
add_library(solvespace_headless STATIC EXCLUDE_FROM_ALL
${solvespace_cad_gl_SOURCES}
${headless_SOURCES})
target_compile_definitions(solvespace_headless
PRIVATE -DHEADLESS)
target_include_directories(solvespace_headless
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(solvespace_headless
solvespace_cad
${CAIRO_LIBRARIES})

View File

@ -302,12 +302,14 @@ void TextWindow::ShowConfiguration() {
Printf(false, "%Ba %d %Fl%Ll%f[change]%E",
SS.autosaveInterval, &ScreenChangeAutosaveInterval);
#if !defined(HEADLESS)
const char *gl_vendor, *gl_renderer, *gl_version;
OpenGl1Renderer::GetIdent(&gl_vendor, &gl_renderer, &gl_version);
Printf(false, "");
Printf(false, " %Ftgl vendor %E%s", gl_vendor);
Printf(false, " %Ft renderer %E%s", gl_renderer);
Printf(false, " %Ft version %E%s", gl_version);
#endif
}
bool TextWindow::EditControlDoneForConfiguration(const char *s) {

View File

@ -710,6 +710,7 @@ void GraphicsWindow::Draw(Canvas *canvas) {
}
void GraphicsWindow::Paint() {
#if !defined(HEADLESS)
havePainted = true;
auto renderStartTime = std::chrono::high_resolution_clock::now();
@ -786,4 +787,5 @@ void GraphicsWindow::Paint() {
}
canvas.EndFrame();
#endif
}

View File

@ -1086,11 +1086,12 @@ void SolveSpaceUI::ExportMeshAsThreeJsTo(FILE *f, const std::string &filename,
// rendering the view in the usual way and then copying the pixels.
//-----------------------------------------------------------------------------
void SolveSpaceUI::ExportAsPngTo(const std::string &filename) {
#if !defined(HEADLESS)
// No guarantee that the back buffer contains anything valid right now,
// so repaint the scene. And hide the toolbar too.
bool prevShowToolbar = SS.showToolbar;
SS.showToolbar = false;
#ifndef WIN32
#if !defined(WIN32)
GlOffscreen offscreen;
offscreen.Render((int)SS.GW.width, (int)SS.GW.height, [&] {
SS.GW.Paint();
@ -1109,10 +1110,11 @@ void SolveSpaceUI::ExportAsPngTo(const std::string &filename) {
}
if(f) fclose(f);
#ifndef WIN32
#if !defined(WIN32)
offscreen.Clear();
#endif
return;
#endif
}

View File

@ -734,34 +734,6 @@ static void PathSepNormalize(std::string &filename)
}
}
static std::string PathSepPlatformToUNIX(const std::string &filename)
{
#if defined(WIN32)
std::string result = filename;
for(size_t i = 0; i < result.length(); i++) {
if(result[i] == '\\')
result[i] = '/';
}
return result;
#else
return filename;
#endif
}
static std::string PathSepUNIXToPlatform(const std::string &filename)
{
#if defined(WIN32)
std::string result = filename;
for(size_t i = 0; i < result.length(); i++) {
if(result[i] == '/')
result[i] = '\\';
}
return result;
#else
return filename;
#endif
}
bool SolveSpaceUI::ReloadAllImported(bool canCancel)
{
std::map<std::string, std::string> linkMap;
@ -791,7 +763,7 @@ bool SolveSpaceUI::ReloadAllImported(bool canCancel)
// In a newly created group we only have an absolute path.
if(!g->linkFileRel.empty()) {
std::string rel = PathSepUNIXToPlatform(g->linkFileRel);
std::string rel = PathSepUnixToPlatform(g->linkFileRel);
std::string fromRel = MakePathAbsolute(SS.saveFile, rel);
FILE *test = ssfopen(fromRel, "rb");
if(test) {
@ -812,7 +784,7 @@ try_load_file:
// Record the linked file's name relative to our filename;
// if the entire tree moves, then everything will still work
std::string rel = MakePathRelative(SS.saveFile, g->linkFile);
g->linkFileRel = PathSepPlatformToUNIX(rel);
g->linkFileRel = PathSepPlatformToUnix(rel);
} else {
// We're not yet saved, so can't make it absolute.
// This will only be used for display purposes, as SS.saveFile

273
src/platform/headless.cpp Normal file
View File

@ -0,0 +1,273 @@
//-----------------------------------------------------------------------------
// Our main() function for the headless (no OpenGL) test runner.
//
// Copyright 2016 whitequark
//-----------------------------------------------------------------------------
#include "solvespace.h"
#include <cairo.h>
namespace SolveSpace {
//-----------------------------------------------------------------------------
// Settings
//-----------------------------------------------------------------------------
class Setting {
public:
enum class Type {
Undefined,
Int,
Float,
String
};
Type type;
int valueInt;
float valueFloat;
std::string valueString;
void CheckType(Type expectedType) {
ssassert(type == Setting::Type::Undefined ||
type == expectedType, "Wrong setting type");
type = expectedType;
}
};
std::map<std::string, Setting> settings;
void CnfFreezeInt(uint32_t val, const std::string &key) {
Setting &setting = settings[key];
setting.CheckType(Setting::Type::Int);
setting.valueInt = val;
}
uint32_t CnfThawInt(uint32_t val, const std::string &key) {
if(settings.find(key) != settings.end()) {
Setting &setting = settings[key];
setting.CheckType(Setting::Type::Int);
val = setting.valueInt;
}
return val;
}
void CnfFreezeFloat(float val, const std::string &key) {
Setting &setting = settings[key];
setting.CheckType(Setting::Type::Float);
setting.valueFloat = val;
}
float CnfThawFloat(float val, const std::string &key) {
if(settings.find(key) != settings.end()) {
Setting &setting = settings[key];
setting.CheckType(Setting::Type::Float);
val = setting.valueFloat;
}
return val;
}
void CnfFreezeString(const std::string &val, const std::string &key) {
Setting &setting = settings[key];
setting.CheckType(Setting::Type::String);
setting.valueString = val;
}
std::string CnfThawString(const std::string &val, const std::string &key) {
std::string ret = val;
if(settings.find(key) != settings.end()) {
Setting &setting = settings[key];
setting.CheckType(Setting::Type::String);
ret = setting.valueString;
}
return ret;
}
//-----------------------------------------------------------------------------
// Timers
//-----------------------------------------------------------------------------
void SetTimerFor(int milliseconds) {
}
void SetAutosaveTimerFor(int minutes) {
}
void ScheduleLater() {
}
//-----------------------------------------------------------------------------
// Graphics window
//-----------------------------------------------------------------------------
void GetGraphicsWindowSize(int *w, int *h) {
*w = *h = 600;
}
void InvalidateGraphics() {
}
std::shared_ptr<Pixmap> framebuffer;
bool antialias = true;
void PaintGraphics() {
const Camera &camera = SS.GW.GetCamera();
std::shared_ptr<Pixmap> pixmap = std::make_shared<Pixmap>();
pixmap->format = Pixmap::Format::BGRA;
pixmap->width = camera.width;
pixmap->height = camera.height;
pixmap->stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, camera.width);
pixmap->data = std::vector<uint8_t>(pixmap->stride * pixmap->height);
cairo_surface_t *surface =
cairo_image_surface_create_for_data(&pixmap->data[0], CAIRO_FORMAT_RGB24,
pixmap->width, pixmap->height, pixmap->stride);
cairo_t *context = cairo_create(surface);
CairoRenderer canvas;
canvas.camera = camera;
canvas.lighting = SS.GW.GetLighting();
canvas.chordTolerance = SS.chordTol;
canvas.context = context;
canvas.antialias = antialias;
SS.GW.Draw(&canvas);
canvas.CullOccludedStrokes();
canvas.OutputInPaintOrder();
pixmap->ConvertTo(Pixmap::Format::RGBA);
framebuffer = pixmap;
canvas.Clear();
cairo_surface_destroy(surface);
cairo_destroy(context);
}
void SetCurrentFilename(const std::string &filename) {
}
void ToggleFullScreen() {
}
bool FullScreenIsActive() {
return false;
}
void ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars,
const std::string &val) {
ssassert(false, "Not implemented");
}
void HideGraphicsEditControl() {
}
bool GraphicsEditControlIsVisible() {
return false;
}
void ToggleMenuBar() {
}
bool MenuBarIsVisible() {
return false;
}
void AddContextMenuItem(const char *label, ContextCommand cmd) {
ssassert(false, "Not implemented");
}
void CreateContextSubmenu() {
ssassert(false, "Not implemented");
}
ContextCommand ShowContextMenu() {
ssassert(false, "Not implemented");
}
void EnableMenuByCmd(Command cmd, bool enabled) {
}
void CheckMenuByCmd(Command cmd, bool checked) {
}
void RadioMenuByCmd(Command cmd, bool selected) {
}
void RefreshRecentMenus() {
}
//-----------------------------------------------------------------------------
// Text window
//-----------------------------------------------------------------------------
void ShowTextWindow(bool visible) {
}
void GetTextWindowSize(int *w, int *h) {
*w = *h = 100;
}
void InvalidateText() {
}
void MoveTextScrollbarTo(int pos, int maxPos, int page) {
}
void SetMousePointerToHand(bool is_hand) {
}
void ShowTextEditControl(int x, int y, const std::string &val) {
ssassert(false, "Not implemented");
}
void HideTextEditControl() {
}
bool TextEditControlIsVisible() {
return false;
}
//-----------------------------------------------------------------------------
// Dialogs
//-----------------------------------------------------------------------------
bool GetOpenFile(std::string *filename, const std::string &activeOrEmpty,
const FileFilter filters[]) {
ssassert(false, "Not implemented");
}
bool GetSaveFile(std::string *filename, const std::string &activeOrEmpty,
const FileFilter filters[]) {
ssassert(false, "Not implemented");
}
DialogChoice SaveFileYesNoCancel() {
ssassert(false, "Not implemented");
}
DialogChoice LoadAutosaveYesNo() {
ssassert(false, "Not implemented");
}
DialogChoice LocateImportedFileYesNoCancel(const std::string &filename,
bool canCancel) {
ssassert(false, "Not implemented");
}
void DoMessageBox(const char *message, int rows, int cols, bool error) {
dbp("%s box: %s", error ? "error" : "message", message);
ssassert(false, "Not implemented");
}
void OpenWebsite(const char *url) {
ssassert(false, "Not implemented");
}
//-----------------------------------------------------------------------------
// Resources
//-----------------------------------------------------------------------------
std::vector<std::string> GetFontFiles() {
return {};
}
std::string resourceDir;
const void *LoadResource(const std::string &name, size_t *size) {
static std::map<std::string, std::vector<uint8_t>> cache;
auto it = cache.find(name);
if(it == cache.end()) {
std::string path = resourceDir + "/" + name;
std::vector<uint8_t> data;
FILE *f = ssfopen(PathSepUnixToPlatform(path).c_str(), "rb");
ssassert(f != NULL, "Cannot open resource");
fseek(f, 0, SEEK_END);
data.resize(ftell(f));
fseek(f, 0, SEEK_SET);
fread(&data[0], 1, data.size(), f);
fclose(f);
cache.emplace(name, std::move(data));
it = cache.find(name);
}
*size = (*it).second.size();
return &(*it).second[0];
}
//-----------------------------------------------------------------------------
// Application lifecycle
//-----------------------------------------------------------------------------
void ExitNow() {
ssassert(false, "Not implemented");
}
}

View File

@ -65,6 +65,16 @@ bool PathEqual(const std::string &a, const std::string &b)
#endif
}
std::string PathSepPlatformToUnix(const std::string &filename)
{
return filename;
}
std::string PathSepUnixToPlatform(const std::string &filename)
{
return filename;
}
FILE *ssfopen(const std::string &filename, const char *mode)
{
ssassert(filename.length() == strlen(filename.c_str()),

View File

@ -97,6 +97,26 @@ bool PathEqual(const std::string &a, const std::string &b)
}
std::string PathSepPlatformToUnix(const std::string &filename)
{
std::string result = filename;
for(size_t i = 0; i < result.length(); i++) {
if(result[i] == '\\')
result[i] = '/';
}
return result;
}
std::string PathSepUnixToPlatform(const std::string &filename)
{
std::string result = filename;
for(size_t i = 0; i < result.length(); i++) {
if(result[i] == '/')
result[i] = '\\';
}
return result;
}
FILE *ssfopen(const std::string &filename, const char *mode)
{
// Prepend \\?\ UNC prefix unless already an UNC path.

View File

@ -277,6 +277,8 @@ public:
class CairoRenderer : public SurfaceRenderer {
public:
cairo_t *context;
// Renderer configuration.
bool antialias;
// Renderer state.
struct {
hStroke hcs;

View File

@ -44,7 +44,11 @@ void CairoRenderer::SelectStroke(hStroke hcs) {
cairo_set_dash(context, dashes.data(), dashes.size(), 0);
cairo_set_source_rgba(context, color.redF(), color.greenF(), color.blueF(),
color.alphaF());
cairo_set_antialias(context, CAIRO_ANTIALIAS_BEST);
if(antialias) {
cairo_set_antialias(context, CAIRO_ANTIALIAS_GRAY);
} else {
cairo_set_antialias(context, CAIRO_ANTIALIAS_NONE);
}
}
void CairoRenderer::MoveTo(Vector p) {

View File

@ -95,6 +95,42 @@ RgbaColor Pixmap::GetPixel(size_t x, size_t y) const {
ssassert(false, "Unexpected resource format");
}
void Pixmap::SetPixel(size_t x, size_t y, RgbaColor color) {
uint8_t *pixel = &data[y * stride + x * GetBytesPerPixel()];
switch(format) {
case Format::RGBA:
pixel[0] = color.red;
pixel[1] = color.green;
pixel[2] = color.blue;
pixel[3] = color.alpha;
break;
case Format::RGB:
pixel[0] = color.red;
pixel[1] = color.green;
pixel[2] = color.blue;
break;
case Format::BGRA:
pixel[0] = color.blue;
pixel[1] = color.green;
pixel[2] = color.red;
pixel[3] = color.alpha;
break;
case Format::BGR:
pixel[0] = color.blue;
pixel[1] = color.green;
pixel[2] = color.red;
break;
case Format::A:
pixel[0] = color.alpha;
break;
}
}
void Pixmap::ConvertTo(Format newFormat) {
switch(format) {
case Format::RGBA:
@ -215,6 +251,14 @@ exit:
return nullptr;
}
std::shared_ptr<Pixmap> Pixmap::ReadPng(const std::string &filename, bool flip) {
FILE *f = ssfopen(filename.c_str(), "rb");
if(!f) return NULL;
std::shared_ptr<Pixmap> pixmap = ReadPng(f, flip);
fclose(f);
return pixmap;
}
bool Pixmap::WritePng(FILE *f, bool flip) {
int colorType;
bool bgr;
@ -262,6 +306,29 @@ exit:
return false;
}
bool Pixmap::WritePng(const std::string &filename, bool flip) {
FILE *f = ssfopen(filename.c_str(), "wb");
if(!f) return false;
bool success = WritePng(f, flip);
fclose(f);
return success;
}
bool Pixmap::Equals(const Pixmap &other) const {
if(format != other.format || width != other.width || height != other.height) {
return false;
}
size_t rowLength = width * GetBytesPerPixel();
for(size_t y = 0; y < height; y++) {
if(memcmp(&data[y * stride], &other.data[y * other.stride], rowLength)) {
return false;
}
}
return true;
}
std::shared_ptr<Pixmap> Pixmap::Create(Format format, size_t width, size_t height) {
std::shared_ptr<Pixmap> pixmap = std::make_shared<Pixmap>();
pixmap->format = format;

View File

@ -36,12 +36,16 @@ public:
static std::shared_ptr<Pixmap> FromPng(const uint8_t *data, size_t size, bool flip = false);
static std::shared_ptr<Pixmap> ReadPng(FILE *f, bool flip = false);
static std::shared_ptr<Pixmap> ReadPng(const std::string &filename, bool flip = false);
bool WritePng(FILE *f, bool flip = false);
bool WritePng(const std::string &filename, bool flip = false);
size_t GetBytesPerPixel() const;
RgbaColor GetPixel(size_t x, size_t y) const;
bool Equals(const Pixmap &other) const;
void ConvertTo(Format newFormat);
void SetPixel(size_t x, size_t y, RgbaColor color);
};
class BitmapFont {

View File

@ -13,8 +13,10 @@ Sketch SolveSpace::SK = {};
std::string SolveSpace::RecentFile[MAX_RECENT] = {};
void SolveSpaceUI::Init() {
#if !defined(HEADLESS)
// Check that the resource system works.
dbp("%s", LoadString("banner.txt").data());
#endif
SS.tangentArcRadius = 10.0;

View File

@ -150,6 +150,8 @@ enum class ContextCommand : uint32_t;
extern const bool FLIP_FRAMEBUFFER;
bool PathEqual(const std::string &a, const std::string &b);
std::string PathSepPlatformToUnix(const std::string &filename);
std::string PathSepUnixToPlatform(const std::string &filename);
FILE *ssfopen(const std::string &filename, const char *mode);
void ssremove(const std::string &filename);

View File

@ -718,6 +718,7 @@ bool TextWindow::DrawOrHitTestColorPicker(UiCanvas *uiCanvas, DrawOrHitHow how,
}
void TextWindow::Paint() {
#if !defined(HEADLESS)
int width, height;
GetTextWindowSize(&width, &height);
@ -857,6 +858,7 @@ void TextWindow::Paint() {
DrawOrHitTestColorPicker(&uiCanvas, PAINT, false, 0, 0);
canvas.EndFrame();
#endif
}
void TextWindow::MouseEvent(bool leftClick, bool leftDown, double x, double y) {

18
test/CMakeLists.txt Normal file
View File

@ -0,0 +1,18 @@
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR})
set(testsuite_SOURCES
harness.cpp
request/line_segment/test.cpp)
add_executable(solvespace_testsuite
${testsuite_SOURCES})
target_link_libraries(solvespace_testsuite
solvespace_headless)
add_custom_target(solvespace-test ALL
COMMAND $<TARGET_FILE:solvespace_testsuite>
COMMENT "Testing SolveSpace"
VERBATIM)

320
test/harness.cpp Normal file
View File

@ -0,0 +1,320 @@
//-----------------------------------------------------------------------------
// Our harness for running test cases, and reusable checks.
//
// Copyright 2016 whitequark
//-----------------------------------------------------------------------------
#include "harness.h"
#include <regex>
#if defined(WIN32)
#include <windows.h>
#else
#include <unistd.h>
#endif
namespace SolveSpace {
// These are defined in headless.cpp, and aren't exposed in solvespace.h.
extern std::string resourceDir;
extern bool antialias;
extern std::shared_ptr<Pixmap> framebuffer;
}
// The paths in __FILE__ are from the build system, but defined(WIN32) returns
// the value for the host system.
#define BUILD_PATH_SEP (__FILE__[0]=='/' ? '/' : '\\')
#define HOST_PATH_SEP PATH_SEP
static std::string BuildRoot() {
static std::string rootDir;
if(!rootDir.empty()) return rootDir;
rootDir = __FILE__;
rootDir.erase(rootDir.rfind(BUILD_PATH_SEP) + 1);
return rootDir;
}
static std::string HostRoot() {
static std::string rootDir;
if(!rootDir.empty()) return rootDir;
// No especially good way to do this, so let's assume somewhere up from
// the current directory there's our repository, with CMakeLists.txt, and
// pivot from there.
#if defined(WIN32)
wchar_t currentDirW[MAX_PATH];
GetCurrentDirectoryW(MAX_PATH, currentDirW);
rootDir = Narrow(currentDirW);
#else
rootDir = ".";
#endif
// We're never more than four levels deep.
for(size_t i = 0; i < 4; i++) {
std::string listsPath = rootDir;
listsPath += HOST_PATH_SEP;
listsPath += "CMakeLists.txt";
FILE *f = ssfopen(listsPath, "r");
if(f) {
fclose(f);
rootDir += HOST_PATH_SEP;
rootDir += "test";
return rootDir;
}
if(rootDir[0] == '.') {
rootDir += HOST_PATH_SEP;
rootDir += "..";
} else {
rootDir.erase(rootDir.rfind(HOST_PATH_SEP));
}
}
ssassert(false, "Couldn't locate repository root");
}
enum class Color {
Red,
Green,
DarkGreen
};
static std::string Colorize(Color color, std::string input) {
#if !defined(WIN32)
if(isatty(fileno(stdout))) {
switch(color) {
case Color::Red:
return "\e[1;31m" + input + "\e[0m";
case Color::Green:
return "\e[1;32m" + input + "\e[0m";
case Color::DarkGreen:
return "\e[36m" + input + "\e[0m";
}
}
#endif
return input;
}
static std::vector<uint8_t> ReadFile(std::string path) {
std::vector<uint8_t> data;
FILE *f = ssfopen(path.c_str(), "rb");
if(f) {
fseek(f, 0, SEEK_END);
data.resize(ftell(f));
fseek(f, 0, SEEK_SET);
fread(&data[0], 1, data.size(), f);
fclose(f);
}
return data;
}
bool Test::Helper::RecordCheck(bool success) {
checkCount++;
if(!success) failCount++;
return success;
}
void Test::Helper::PrintFailure(const char *file, int line, std::string msg) {
std::string shortFile = file;
shortFile.erase(0, BuildRoot().size());
fprintf(stderr, "test%c%s:%d: FAILED: %s\n",
BUILD_PATH_SEP, shortFile.c_str(), line, msg.c_str());
}
std::string Test::Helper::GetAssetPath(std::string testFile, std::string assetName,
std::string mangle) {
if(!mangle.empty()) {
assetName.insert(assetName.rfind('.'), "." + mangle);
}
testFile.erase(0, BuildRoot().size());
testFile.erase(testFile.rfind(BUILD_PATH_SEP) + 1);
return PathSepUnixToPlatform(HostRoot() + "/" + testFile + assetName);
}
bool Test::Helper::CheckTrue(const char *file, int line, const char *expr, bool result) {
if(!RecordCheck(result)) {
PrintFailure(file, line,
ssprintf("(%s) == %s", expr, result ? "true" : "false"));
return false;
} else {
return true;
}
}
bool Test::Helper::CheckLoad(const char *file, int line, const char *fixture) {
std::string fixturePath = GetAssetPath(file, fixture);
FILE *f = ssfopen(fixturePath.c_str(), "rb");
bool fixtureExists = (f != NULL);
if(f) fclose(f);
bool result = fixtureExists &&
SS.LoadFromFile(fixturePath) && SS.ReloadAllImported(/*canCancel=*/false);
if(!RecordCheck(result)) {
PrintFailure(file, line,
ssprintf("loading file '%s'", fixturePath.c_str()));
return false;
} else {
SS.AfterNewFile();
SS.GW.offset = {};
SS.GW.scale = 10.0;
return true;
}
}
bool Test::Helper::CheckSave(const char *file, int line, const char *reference) {
std::string refPath = GetAssetPath(file, reference),
outPath = GetAssetPath(file, reference, "out");
if(!RecordCheck(SS.SaveToFile(outPath))) {
PrintFailure(file, line,
ssprintf("saving file '%s'", refPath.c_str()));
return false;
} else {
std::vector<uint8_t> refData = ReadFile(refPath),
outData = ReadFile(outPath);
if(!RecordCheck(refData == outData)) {
PrintFailure(file, line, "savefile doesn't match reference");
return false;
}
ssremove(outPath);
return true;
}
}
bool Test::Helper::CheckRender(const char *file, int line, const char *reference) {
PaintGraphics();
std::string refPath = GetAssetPath(file, reference),
outPath = GetAssetPath(file, reference, "out"),
diffPath = GetAssetPath(file, reference, "diff");
std::shared_ptr<Pixmap> refPixmap = Pixmap::ReadPng(refPath.c_str(), /*flip=*/true);
if(!RecordCheck(refPixmap && refPixmap->Equals(*framebuffer))) {
framebuffer->WritePng(outPath.c_str(), /*flip=*/true);
if(!refPixmap) {
PrintFailure(file, line, "reference render not present");
return false;
}
ssassert(refPixmap->format == framebuffer->format, "Expected buffer formats to match");
if(refPixmap->width != framebuffer->width ||
refPixmap->height != framebuffer->height) {
PrintFailure(file, line, "render doesn't match reference; dimensions differ");
} else {
std::shared_ptr<Pixmap> diffPixmap =
Pixmap::Create(refPixmap->format, refPixmap->width, refPixmap->height);
int diffPixelCount = 0;
for(size_t j = 0; j < refPixmap->height; j++) {
for(size_t i = 0; i < refPixmap->width; i++) {
if(!refPixmap->GetPixel(i, j).Equals(framebuffer->GetPixel(i, j))) {
diffPixelCount++;
diffPixmap->SetPixel(i, j, RgbaColor::From(255, 0, 0, 255));
}
}
}
diffPixmap->WritePng(diffPath.c_str(), /*flip=*/true);
std::string message =
ssprintf("render doesn't match reference; %d (%.2f%%) pixels differ",
diffPixelCount,
100.0 * diffPixelCount / (refPixmap->width * refPixmap->height));
PrintFailure(file, line, message);
}
return false;
} else {
ssremove(outPath);
ssremove(diffPath);
return true;
}
}
// Avoid global constructors; using a global static vector instead of a local one
// breaks MinGW for some obscure reason.
static std::vector<Test::Case> *testCasesPtr;
int Test::Case::Register(Test::Case testCase) {
static std::vector<Test::Case> testCases;
testCases.push_back(testCase);
testCasesPtr = &testCases;
return 0;
}
int main(int argc, char **argv) {
#if defined(WIN32)
_set_abort_behavior(0, _WRITE_ABORT_MSG);
InitHeaps();
#endif
std::regex filter(".*");
if(argc == 1) {
} else if(argc == 2) {
filter = argv[1];
} else {
fprintf(stderr, "Usage: %s [test filter regex]\n", argv[0]);
return 1;
}
resourceDir = HostRoot();
resourceDir.erase(resourceDir.rfind(HOST_PATH_SEP) + 1);
resourceDir += "res";
// Different Cairo versions have different antialiasing algorithms.
antialias = false;
// Wreck order dependencies between tests!
std::random_shuffle(testCasesPtr->begin(), testCasesPtr->end());
auto testStartTime = std::chrono::steady_clock::now();
size_t ranTally = 0, skippedTally = 0, checkTally = 0, failTally = 0;
for(Test::Case &testCase : *testCasesPtr) {
std::string testCaseName = testCase.fileName;
testCaseName.erase(0, BuildRoot().size());
testCaseName.erase(testCaseName.rfind(BUILD_PATH_SEP));
testCaseName += BUILD_PATH_SEP + testCase.caseName;
std::smatch filterMatch;
if(!std::regex_search(testCaseName, filterMatch, filter)) {
skippedTally += 1;
continue;
}
SS.Init();
Test::Helper helper = {};
testCase.fn(&helper);
SK.Clear();
SS.Clear();
ranTally += 1;
checkTally += helper.checkCount;
failTally += helper.failCount;
if(helper.checkCount == 0) {
fprintf(stderr, " %s test %s (empty)\n",
Colorize(Color::Red, "??").c_str(),
Colorize(Color::DarkGreen, testCaseName).c_str());
} else if(helper.failCount > 0) {
fprintf(stderr, " %s test %s\n",
Colorize(Color::Red, "NG").c_str(),
Colorize(Color::DarkGreen, testCaseName).c_str());
} else {
fprintf(stderr, " %s test %s\n",
Colorize(Color::Green, "OK").c_str(),
Colorize(Color::DarkGreen, testCaseName).c_str());
}
}
auto testEndTime = std::chrono::steady_clock::now();
std::chrono::duration<double> testTime = testEndTime - testStartTime;
if(failTally > 0) {
fprintf(stderr, "Failure! %u checks failed\n",
(unsigned)failTally);
return 1;
} else {
fprintf(stderr, "Success! %u test cases (%u skipped), %u checks, %.3fs\n",
(unsigned)ranTally, (unsigned)skippedTally,
(unsigned)checkTally, testTime.count());
return 0;
}
}

57
test/harness.h Normal file
View File

@ -0,0 +1,57 @@
//-----------------------------------------------------------------------------
// Our harness for running test cases, and reusable checks.
//
// Copyright 2016 whitequark
//-----------------------------------------------------------------------------
#include "solvespace.h"
// Hack... we should rename the one in ui.h instead.
#undef CHECK_TRUE
namespace SolveSpace {
namespace Test {
class Helper {
public:
size_t checkCount;
size_t failCount;
bool RecordCheck(bool success);
void PrintFailure(const char *file, int line, std::string msg);
std::string GetAssetPath(std::string testFile, std::string assetName,
std::string mangle = "");
bool CheckTrue(const char *file, int line, const char *expr, bool result);
bool CheckLoad(const char *file, int line, const char *fixture);
bool CheckSave(const char *file, int line, const char *reference);
bool CheckRender(const char *file, int line, const char *fixture);
};
class Case {
public:
std::string fileName;
std::string caseName;
std::function<void(Helper *)> fn;
static int Register(Case testCase);
};
}
}
using namespace SolveSpace;
#define TEST_CASE(name) \
static void Test_##name(Test::Helper *); \
static Test::Case TestCase_##name = { __FILE__, #name, Test_##name }; \
static int TestReg_##name = Test::Case::Register(TestCase_##name); \
static void Test_##name(Test::Helper *helper) // { ... }
#define CHECK_TRUE(cond) \
do { if(!helper->CheckTrue(__FILE__, __LINE__, #cond, cond)) return; } while(0)
#define CHECK_LOAD(fixture) \
do { if(!helper->CheckLoad(__FILE__, __LINE__, fixture)) return; } while(0)
#define CHECK_SAVE(fixture) \
do { if(!helper->CheckSave(__FILE__, __LINE__, fixture)) return; } while(0)
#define CHECK_RENDER(reference) \
do { if(!helper->CheckRender(__FILE__, __LINE__, reference)) return; } while(0)

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,278 @@
±²³SolveSpaceREVa
Group.h.v=00000001
Group.type=5000
Group.name=#references
Group.skipFirst=0
Group.predef.swapUV=0
Group.predef.negateU=0
Group.predef.negateV=0
Group.visible=1
Group.suppress=0
Group.relaxConstraints=0
Group.allDimsReference=0
Group.scale=1.00000000000000000000
Group.remap={
}
Group.impFile=
Group.impFileRel=
AddGroup
Group.h.v=00000002
Group.type=5001
Group.order=1
Group.name=sketch-in-plane
Group.activeWorkplane.v=80020000
Group.subtype=6000
Group.skipFirst=0
Group.predef.q.w=1.00000000000000000000
Group.predef.origin.v=00010001
Group.predef.swapUV=0
Group.predef.negateU=0
Group.predef.negateV=0
Group.visible=1
Group.suppress=0
Group.relaxConstraints=0
Group.allDimsReference=0
Group.scale=1.00000000000000000000
Group.remap={
}
Group.impFile=
Group.impFileRel=
AddGroup
Param.h.v.=00010010
AddParam
Param.h.v.=00010011
AddParam
Param.h.v.=00010012
AddParam
Param.h.v.=00010020
Param.val=1.00000000000000000000
AddParam
Param.h.v.=00010021
AddParam
Param.h.v.=00010022
AddParam
Param.h.v.=00010023
AddParam
Param.h.v.=00020010
AddParam
Param.h.v.=00020011
AddParam
Param.h.v.=00020012
AddParam
Param.h.v.=00020020
Param.val=0.50000000000000000000
AddParam
Param.h.v.=00020021
Param.val=0.50000000000000000000
AddParam
Param.h.v.=00020022
Param.val=0.50000000000000000000
AddParam
Param.h.v.=00020023
Param.val=0.50000000000000000000
AddParam
Param.h.v.=00030010
AddParam
Param.h.v.=00030011
AddParam
Param.h.v.=00030012
AddParam
Param.h.v.=00030020
Param.val=0.50000000000000000000
AddParam
Param.h.v.=00030021
Param.val=-0.50000000000000000000
AddParam
Param.h.v.=00030022
Param.val=-0.50000000000000000000
AddParam
Param.h.v.=00030023
Param.val=-0.50000000000000000000
AddParam
Param.h.v.=00040010
Param.val=-5.00000000000000000000
AddParam
Param.h.v.=00040011
Param.val=5.00000000000000000000
AddParam
Param.h.v.=00040013
Param.val=5.00000000000000000000
AddParam
Param.h.v.=00040014
Param.val=5.00000000000000000000
AddParam
Request.h.v=00000001
Request.type=100
Request.group.v=00000001
Request.construction=0
AddRequest
Request.h.v=00000002
Request.type=100
Request.group.v=00000001
Request.construction=0
AddRequest
Request.h.v=00000003
Request.type=100
Request.group.v=00000001
Request.construction=0
AddRequest
Request.h.v=00000004
Request.type=200
Request.workplane.v=80020000
Request.group.v=00000002
Request.construction=0
AddRequest
Entity.h.v=00010000
Entity.type=10000
Entity.construction=0
Entity.point[0].v=00010001
Entity.normal.v=00010020
Entity.actVisible=1
AddEntity
Entity.h.v=00010001
Entity.type=2000
Entity.construction=0
Entity.actVisible=1
AddEntity
Entity.h.v=00010020
Entity.type=3000
Entity.construction=0
Entity.point[0].v=00010001
Entity.actNormal.w=1.00000000000000000000
Entity.actVisible=1
AddEntity
Entity.h.v=00020000
Entity.type=10000
Entity.construction=0
Entity.point[0].v=00020001
Entity.normal.v=00020020
Entity.actVisible=1
AddEntity
Entity.h.v=00020001
Entity.type=2000
Entity.construction=0
Entity.actVisible=1
AddEntity
Entity.h.v=00020020
Entity.type=3000
Entity.construction=0
Entity.point[0].v=00020001
Entity.actNormal.w=0.50000000000000000000
Entity.actNormal.vx=0.50000000000000000000
Entity.actNormal.vy=0.50000000000000000000
Entity.actNormal.vz=0.50000000000000000000
Entity.actVisible=1
AddEntity
Entity.h.v=00030000
Entity.type=10000
Entity.construction=0
Entity.point[0].v=00030001
Entity.normal.v=00030020
Entity.actVisible=1
AddEntity
Entity.h.v=00030001
Entity.type=2000
Entity.construction=0
Entity.actVisible=1
AddEntity
Entity.h.v=00030020
Entity.type=3000
Entity.construction=0
Entity.point[0].v=00030001
Entity.actNormal.w=0.50000000000000000000
Entity.actNormal.vx=-0.50000000000000000000
Entity.actNormal.vy=-0.50000000000000000000
Entity.actNormal.vz=-0.50000000000000000000
Entity.actVisible=1
AddEntity
Entity.h.v=00040000
Entity.type=11000
Entity.construction=0
Entity.point[0].v=00040001
Entity.point[1].v=00040002
Entity.workplane.v=80020000
Entity.actVisible=1
AddEntity
Entity.h.v=00040001
Entity.type=2001
Entity.construction=0
Entity.workplane.v=80020000
Entity.actPoint.x=-5.00000000000000000000
Entity.actPoint.y=5.00000000000000000000
Entity.actVisible=1
AddEntity
Entity.h.v=00040002
Entity.type=2001
Entity.construction=0
Entity.workplane.v=80020000
Entity.actPoint.x=5.00000000000000000000
Entity.actPoint.y=5.00000000000000000000
Entity.actVisible=1
AddEntity
Entity.h.v=80020000
Entity.type=10000
Entity.construction=0
Entity.point[0].v=80020002
Entity.normal.v=80020001
Entity.actVisible=1
AddEntity
Entity.h.v=80020001
Entity.type=3010
Entity.construction=0
Entity.point[0].v=80020002
Entity.actNormal.w=1.00000000000000000000
Entity.actVisible=1
AddEntity
Entity.h.v=80020002
Entity.type=2012
Entity.construction=0
Entity.actVisible=1
AddEntity

View File

@ -0,0 +1,278 @@
±²³SolveSpaceREVa
Group.h.v=00000001
Group.type=5000
Group.name=#references
Group.color=ff000000
Group.skipFirst=0
Group.predef.swapUV=0
Group.predef.negateU=0
Group.predef.negateV=0
Group.visible=1
Group.suppress=0
Group.relaxConstraints=0
Group.allowRedundant=0
Group.allDimsReference=0
Group.scale=1.00000000000000000000
Group.remap={
}
AddGroup
Group.h.v=00000002
Group.type=5001
Group.order=1
Group.name=sketch-in-plane
Group.activeWorkplane.v=80020000
Group.color=ff000000
Group.subtype=6000
Group.skipFirst=0
Group.predef.q.w=1.00000000000000000000
Group.predef.origin.v=00010001
Group.predef.swapUV=0
Group.predef.negateU=0
Group.predef.negateV=0
Group.visible=1
Group.suppress=0
Group.relaxConstraints=0
Group.allowRedundant=0
Group.allDimsReference=0
Group.scale=1.00000000000000000000
Group.remap={
}
AddGroup
Param.h.v.=00010010
AddParam
Param.h.v.=00010011
AddParam
Param.h.v.=00010012
AddParam
Param.h.v.=00010020
Param.val=1.00000000000000000000
AddParam
Param.h.v.=00010021
AddParam
Param.h.v.=00010022
AddParam
Param.h.v.=00010023
AddParam
Param.h.v.=00020010
AddParam
Param.h.v.=00020011
AddParam
Param.h.v.=00020012
AddParam
Param.h.v.=00020020
Param.val=0.50000000000000000000
AddParam
Param.h.v.=00020021
Param.val=0.50000000000000000000
AddParam
Param.h.v.=00020022
Param.val=0.50000000000000000000
AddParam
Param.h.v.=00020023
Param.val=0.50000000000000000000
AddParam
Param.h.v.=00030010
AddParam
Param.h.v.=00030011
AddParam
Param.h.v.=00030012
AddParam
Param.h.v.=00030020
Param.val=0.50000000000000000000
AddParam
Param.h.v.=00030021
Param.val=-0.50000000000000000000
AddParam
Param.h.v.=00030022
Param.val=-0.50000000000000000000
AddParam
Param.h.v.=00030023
Param.val=-0.50000000000000000000
AddParam
Param.h.v.=00040010
Param.val=-5.00000000000000000000
AddParam
Param.h.v.=00040011
Param.val=5.00000000000000000000
AddParam
Param.h.v.=00040013
Param.val=5.00000000000000000000
AddParam
Param.h.v.=00040014
Param.val=5.00000000000000000000
AddParam
Request.h.v=00000001
Request.type=100
Request.group.v=00000001
Request.construction=0
AddRequest
Request.h.v=00000002
Request.type=100
Request.group.v=00000001
Request.construction=0
AddRequest
Request.h.v=00000003
Request.type=100
Request.group.v=00000001
Request.construction=0
AddRequest
Request.h.v=00000004
Request.type=200
Request.workplane.v=80020000
Request.group.v=00000002
Request.construction=0
AddRequest
Entity.h.v=00010000
Entity.type=10000
Entity.construction=0
Entity.point[0].v=00010001
Entity.normal.v=00010020
Entity.actVisible=1
AddEntity
Entity.h.v=00010001
Entity.type=2000
Entity.construction=0
Entity.actVisible=1
AddEntity
Entity.h.v=00010020
Entity.type=3000
Entity.construction=0
Entity.point[0].v=00010001
Entity.actNormal.w=1.00000000000000000000
Entity.actVisible=1
AddEntity
Entity.h.v=00020000
Entity.type=10000
Entity.construction=0
Entity.point[0].v=00020001
Entity.normal.v=00020020
Entity.actVisible=1
AddEntity
Entity.h.v=00020001
Entity.type=2000
Entity.construction=0
Entity.actVisible=1
AddEntity
Entity.h.v=00020020
Entity.type=3000
Entity.construction=0
Entity.point[0].v=00020001
Entity.actNormal.w=0.50000000000000000000
Entity.actNormal.vx=0.50000000000000000000
Entity.actNormal.vy=0.50000000000000000000
Entity.actNormal.vz=0.50000000000000000000
Entity.actVisible=1
AddEntity
Entity.h.v=00030000
Entity.type=10000
Entity.construction=0
Entity.point[0].v=00030001
Entity.normal.v=00030020
Entity.actVisible=1
AddEntity
Entity.h.v=00030001
Entity.type=2000
Entity.construction=0
Entity.actVisible=1
AddEntity
Entity.h.v=00030020
Entity.type=3000
Entity.construction=0
Entity.point[0].v=00030001
Entity.actNormal.w=0.50000000000000000000
Entity.actNormal.vx=-0.50000000000000000000
Entity.actNormal.vy=-0.50000000000000000000
Entity.actNormal.vz=-0.50000000000000000000
Entity.actVisible=1
AddEntity
Entity.h.v=00040000
Entity.type=11000
Entity.construction=0
Entity.point[0].v=00040001
Entity.point[1].v=00040002
Entity.workplane.v=80020000
Entity.actVisible=1
AddEntity
Entity.h.v=00040001
Entity.type=2001
Entity.construction=0
Entity.workplane.v=80020000
Entity.actPoint.x=-5.00000000000000000000
Entity.actPoint.y=5.00000000000000000000
Entity.actVisible=1
AddEntity
Entity.h.v=00040002
Entity.type=2001
Entity.construction=0
Entity.workplane.v=80020000
Entity.actPoint.x=5.00000000000000000000
Entity.actPoint.y=5.00000000000000000000
Entity.actVisible=1
AddEntity
Entity.h.v=80020000
Entity.type=10000
Entity.construction=0
Entity.point[0].v=80020002
Entity.normal.v=80020001
Entity.actVisible=1
AddEntity
Entity.h.v=80020001
Entity.type=3010
Entity.construction=0
Entity.point[0].v=80020002
Entity.actNormal.w=1.00000000000000000000
Entity.actVisible=1
AddEntity
Entity.h.v=80020002
Entity.type=2012
Entity.construction=0
Entity.actVisible=1
AddEntity

View File

@ -0,0 +1,17 @@
#include "harness.h"
TEST_CASE(load_v20) {
CHECK_LOAD("line_segment_v20.slvs");
CHECK_RENDER("line_segment.png");
}
TEST_CASE(roundtrip_v21) {
CHECK_LOAD("line_segment_v21.slvs");
CHECK_RENDER("line_segment.png");
CHECK_SAVE("line_segment_v21.slvs");
}
TEST_CASE(migrate_v20_to_v21) {
CHECK_LOAD("line_segment_v20.slvs");
CHECK_SAVE("line_segment_v21.slvs");
}