diff --git a/.plans/2026-05-22-ownership-import-hardening.md b/.plans/2026-05-22-ownership-import-hardening.md new file mode 100644 index 0000000..9b60c17 --- /dev/null +++ b/.plans/2026-05-22-ownership-import-hardening.md @@ -0,0 +1,342 @@ +# WonderBrush-v3 Ownership and Import Hardening Plan + +> For Hermes: implement task-by-task using strict Red/Green TDD. Do not change production code without first adding and running a failing focused test. + +Goal: harden WonderBrush-v3’s highest-risk ownership/refcount and file import/export paths while preserving Haiku-first semantics. + +Architecture: start with a small Linux-friendly test harness for platform-independent C++ units. Use it to drive ownership fixes around `Referenceable`, `Layer`, `NotifyingList`, `CloneContext`, and edit objects. Then add importer/exporter tests for bounded parsing/formatting behavior. The existing Qt/Linux target is useful as an optional compile smoke test if Qt/qmake is installed, but it is not currently available on this NUC. + +Tech stack: C++ using existing Haiku-style classes and the incomplete Qt compatibility layer where needed; `g++`/`make` for focused Linux tests; Haiku `jam` remains the final platform validation path. + +--- + +## Testing answer: does the Linux/Qt target help? + +Yes, but with limits. + +The theoretical Linux/Qt target helps as a compile smoke test and may catch accidental breakage in shared model/render/import code. It does not prove Haiku runtime behavior, and the README explicitly says the Qt port is incomplete/broken. + +Current NUC state: + +- Available: `g++`, `make` +- Missing: `qmake`, `qmake6`, `qmake-qt5`, `jam` +- No existing test files were found. + +Therefore the practical TDD strategy is: + +1. Create small focused command-line C++ tests under `tests/`. +2. Compile only the minimal source subset required for each bug. +3. Use the Qt compatibility headers only when unavoidable. +4. Add optional qmake/Qt smoke instructions, but do not depend on them initially. +5. Keep Haiku/jam validation as a later manual or CI step on Haiku. + +--- + +## Phase 0: Test harness and contributor context + +### Task 0.1: Add `AGENTS.md` + +Objective: document Haiku-first context, strict TDD, and local test strategy. + +Files: +- Create: `AGENTS.md` + +Steps: +1. Write `AGENTS.md` with project context and Red/Green TDD requirements. +2. Verify it exists: `test -f AGENTS.md`. +3. Commit: `docs: add contributor context for hardening branch`. + +Status: done in working tree; commit pending. + +### Task 0.2: Add minimal local test harness + +Objective: provide a Linux-friendly `make -C tests` entry point before touching production code. + +Files: +- Create: `tests/Makefile` +- Create: `tests/support/Test.h` +- Create: `tests/ownership/ReferenceableLifecycleTest.cpp` + +RED: +- Add a test executable that asserts `Referenceable` starts with one reference and that `Reference(obj, true)` adopts without incrementing. +- Run: `make -C tests ownership-referenceable` +- Expected initial failure before harness exists: no target/file. + +GREEN: +- Add harness and compile the test with: + - `src/support/Referenceable.cpp` + - include paths for `src/support` and `src/platform/qt/system/include` +- Run: `make -C tests ownership-referenceable` +- Expected: pass. + +REFACTOR: +- Keep harness tiny and dependency-free. + +Commit: `test: add focused C++ ownership test harness` + +--- + +## Phase 1: Ownership/refcount fixes + +### Task 1.1: Prove `CopyCloneContext` adopts freshly cloned resources + +Objective: catch the extra-reference leak in `CopyCloneContext::Clone()`. + +Files: +- Test: `tests/ownership/CopyCloneContextTest.cpp` +- Modify: `src/model/CopyCloneContext.cpp` + +RED: +- Create a minimal `BaseObject` test double whose destructor increments a counter and whose `Clone()` returns a new object with initial refcount 1. +- Call `CopyCloneContext::Clone(original)` and release the returned `BaseObjectRef`. +- Assert the clone is destroyed when the returned reference goes out of scope. +- Run: `make -C tests ownership-copy-clone-context` +- Expected: FAIL because clone still has an extra reference. + +GREEN: +- Change `CopyCloneContext::Clone()` to construct the returned reference with already-owned/adopt semantics, likely `BaseObjectRef(object->Clone(*this), true)`. +- Run focused test; expected PASS. +- Run all local tests: `make -C tests`. + +Commit: `fix: adopt cloned resources in CopyCloneContext` + +### Task 1.2: Prove `Layer::CloneObjects()` releases the creator reference after insertion + +Objective: catch leaks when cloned layer children are inserted into the target layer. + +Files: +- Test: `tests/ownership/LayerCloneObjectsTest.cpp` +- Modify: `src/model/objects/Layer.cpp` + +RED: +- Add a minimal cloneable `Object`/`Layer` fixture if feasible with existing model classes. +- Clone a layer containing one child object. +- Destroy/release the cloned layer. +- Assert the cloned child destructor ran. +- Run: `make -C tests ownership-layer-clone-objects` +- Expected: FAIL due to one extra reference after `target->AddObject(clone)`. + +GREEN: +- After successful `target->AddObject(clone)`, release the initial clone reference with `clone->RemoveReference()` or introduce an explicit adopt helper. +- On insertion failure, continue to delete/release correctly. +- Run focused test and all local tests. + +Commit: `fix: release creator reference after cloning layer objects` + +Status: done in commit `fix: release creator reference after cloning layer objects`. + +### Task 1.3: Prove `Document::Clone()` does not leak global resource clones + +Objective: catch the same AddObject/initial-reference mismatch for global resources. + +Files: +- Test: `tests/ownership/DocumentCloneResourcesTest.cpp` +- Modify: `src/model/document/Document.cpp` + +RED: +- Create a document/global-resource fixture with one cloneable resource. +- Clone the document/resource list. +- Release clone references and assert cloned resource destructor ran. +- Run: `make -C tests ownership-document-clone-resources` +- Expected: FAIL from extra reference. + +GREEN: +- After `target.AddObject(clone)` succeeds, release the clone creator reference. +- Run focused and all local tests. + +Commit: `fix: release creator reference after cloning document resources` + +### Task 1.4: Make remove APIs safe against dangling return values + +Objective: avoid APIs that return a raw pointer after potentially deleting it. + +Files: +- Test: `tests/ownership/RemoveObjectReturnTest.cpp` +- Modify: `src/model/objects/Layer.h` +- Modify: `src/model/objects/Layer.cpp` +- Modify: `src/model/document/NotifyingList.h` +- Modify call sites found by compiler/search. + +RED: +- Add tests that remove an object whose only owner is the container. +- Assert removal result does not expose a raw pointer that may be dead. Prefer changing API to `bool RemoveObject(...)` for non-retaining removal. +- Run focused test; expected FAIL or compile failure against old API. + +GREEN: +- Change removal API to return `bool` where callers only need success. +- If any caller needs the object, add an explicit retaining function, e.g. `Reference RemoveObjectRetaining(...)`. +- Run focused and all local tests. + +Commit: `fix: avoid dangling pointers from remove APIs` + +### Task 1.5: Guard duplicate insertion into `Layer` + +Objective: make `Layer::AddObject()` match `NotifyingList::AddObject()` behavior and avoid duplicate raw object entries. + +Files: +- Test: `tests/ownership/LayerDuplicateInsertTest.cpp` +- Modify: `src/model/objects/Layer.cpp` + +RED: +- Insert the same object into a layer twice. +- Assert second insertion fails and object count remains one. +- Run focused test; expected FAIL because current `Layer::AddObject()` allows duplicates. + +GREEN: +- Add `fObjects.HasItem(object)` guard before insertion. +- Run focused and all local tests. + +Commit: `fix: reject duplicate objects in layers` + +--- + +## Phase 2: Undo/redo safety + +### Task 2.1: Initialize all `MoveObjectsEdit` position slots + +Objective: remove undefined behavior from memcmp over uninitialized `PositionInfo` entries. + +Files: +- Test: `tests/edits/MoveObjectsEditTest.cpp` +- Modify: `src/edits/MoveObjectsEdit.cpp` + +RED: +- Build a nested-layer move scenario where a selected child is ignored because its parent layer is also selected. +- Run under normal test and, if available, with `-fsanitize=address,undefined`. +- Expected: FAIL or sanitizer warning before fix. + +GREEN: +- Value-initialize all `PositionInfo` arrays. +- Avoid `memcmp` over structs if padding exists; compare fields explicitly. +- Run focused and all local tests. + +Commit: `fix: initialize MoveObjectsEdit positions` + +### Task 2.2: Add rollback on partial add failure + +Objective: make multi-object add/move edit operations transactional. + +Files: +- Test: `tests/edits/AddObjectRollbackTest.cpp` +- Modify: `src/edits/AddObjectsEdit.h` +- Modify: `src/edits/MoveObjectsEdit.cpp` + +RED: +- Use a test layer or injection point that fails an insertion after the first object succeeds. +- Assert the original object tree is restored. +- Expected: FAIL due to partial mutation. + +GREEN: +- Track successful insertions and roll them back on failure. +- Run focused and all local tests. + +Commit: `fix: roll back partial edit insertions` + +--- + +## Phase 3: Import/export hardening + +### Task 3.1: Harden SVG error formatting + +Objective: replace unsafe `sprintf`/`vsprintf` in SVG import error paths. + +Files: +- Test: `tests/import_export/SVGErrorFormattingTest.cpp` +- Modify: `src/import_export/svg/SVGImporter.cpp` +- Modify: `src/import_export/svg/SVGParser.cpp` +- Modify: `src/import_export/svg/SVGException.h` + +RED: +- Create long path/error-string inputs. +- Assert formatting is bounded and null-terminated. +- Expected: FAIL or sanitizer failure with existing code. + +GREEN: +- Use `snprintf`/`vsnprintf` or dynamic string storage. +- Run focused and all local tests. + +Commit: `fix: bound SVG importer error formatting` + +### Task 3.2: Harden SVG `url(...)` parsing + +Objective: stop out-of-bounds reads for malformed URL references. + +Files: +- Test: `tests/import_export/SVGUrlParsingTest.cpp` +- Modify: `src/import_export/svg/SVGParser.cpp` +- Possibly modify: `src/import_export/svg/DocumentBuilder.*` + +RED: +- Test malformed inputs: `url(foo`, `url(nohash)`, `url(#unterminated`, empty string. +- Expected: FAIL or sanitizer warning. + +GREEN: +- Parse within string bounds; return false/error on malformed URLs. +- Replace fixed 64-byte URL buffers in `DocumentBuilder` with dynamic strings or bounded copy. +- Run focused and all local tests. + +Commit: `fix: validate SVG URL references` + +### Task 3.3: Harden flat-icon buffer movement + +Objective: make `LittleEndianBuffer` safe for zero-length reads, skip bounds, and realloc failure. + +Files: +- Test: `tests/icon/LittleEndianBufferTest.cpp` +- Modify: `src/icon/flat_icon/LittleEndianBuffer.cpp` +- Modify: `src/icon/flat_icon/LittleEndianBuffer.h` if needed + +RED: +- Test `Read(..., 0)`, `Skip()` past end, and write-growth failure if injectable. +- Expected: FAIL for unchecked skip/zero read behavior. + +GREEN: +- Add bounds checks. +- Use temp pointer for `realloc`. +- Return status rather than silently moving beyond buffer. +- Run focused and all local tests. + +Commit: `fix: validate flat icon buffer bounds` + +### Task 3.4: Restore atomic export semantics + +Objective: avoid direct destructive overwrite during export. + +Files: +- Test: `tests/import_export/ExporterAtomicSaveTest.cpp` +- Modify: `src/import_export/Exporter.cpp` + +RED: +- Simulate an export failure after opening/writing temp output. +- Assert original target file content remains unchanged. +- Expected: FAIL because current code opens target with `B_ERASE_FILE`. + +GREEN: +- Write to temp file in same directory, then rename/replace only after successful export. +- Run focused and all local tests. + +Commit: `fix: save exports atomically` + +--- + +## Verification checkpoints + +After each task: + +1. Focused test fails before production change. +2. Focused test passes after production change. +3. `make -C tests` passes. +4. `git status --short` contains only intended files before commit. +5. Commit with conventional message. + +Optional broader checks when tools exist: + +- Linux/Qt smoke: `qmake wonderbrush2.pro && make -j2` +- Haiku smoke: `jam -q -j2` +- Haiku debug: `DEBUG=1 jam -q -j2` + +--- + +## First implementation slice + +Start with Phase 0.2, then Task 1.1. Do not attempt the larger Layer/Document API changes until the test harness proves it can compile focused ownership tests locally. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..90eac58 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,66 @@ +# AGENTS.md — WonderBrush-v3 contributor context + +## Project context + +WonderBrush-v3 is a work-in-progress rewrite of WonderBrush for Haiku. Treat Haiku as the primary platform and API target. Do not "fix" Haiku-specific APIs by replacing them with Linux abstractions unless the change is explicitly scoped to the existing Qt/Linux compatibility layer. + +Important architectural pieces: + +- `src/model`: document model, resources, objects, layers, properties, snapshots. +- `src/render`: asynchronous snapshot/render pipeline, render buffers, text layout/rendering. +- `src/edits`: undo/redo edits. +- `src/import_export`: document, SVG, bitmap, flat-icon and styled-text import/export. +- `src/platform/qt`: incomplete Linux/Qt compatibility layer for development and experimentation. +- `src/gui/haiku`: Haiku UI implementation. + +The current local checkout is on an Ubuntu NUC, so full Haiku runtime validation is not available here. Prefer portable/unit-level tests for code touched on Linux, and document when final validation still needs Haiku. + +## Development rules + +1. Use Red/Green TDD for production code changes. + - RED: write a focused failing automated test first. + - Verify RED by running the test and seeing it fail for the expected reason. + - GREEN: write the smallest production-code change that makes it pass. + - Verify GREEN by running the focused test and then the relevant broader test/build command. + - REFACTOR only after tests are green. + +2. Do not change production code without a failing test first, except for pure documentation or build/test harness setup. + +3. Keep changes small and commit at logical checkpoints. + +4. Preserve Haiku semantics. Linux/Qt tests are a safety net, not the product definition. + +5. Be careful with ownership. Many model objects inherit from `Referenceable`, which starts with refcount 1. Container APIs such as `Layer::AddObject()` and `NotifyingList::AddObject()` currently add their own reference. Always make ownership transfer explicit in code and tests. + +6. Treat importers as untrusted-input code. Avoid unchecked `sprintf`, unchecked pointer arithmetic, unchecked lengths, and direct target-file overwrite on save/export. + +## Local test strategy + +There is no existing test suite in this checkout. The Linux/Qt target can help in two ways: + +1. If Qt/qmake is installed, use the existing qmake project as a compile smoke test for the Qt port: + - `qmake wonderbrush2.pro` + - `make -j2` + +2. For TDD on this Ubuntu NUC, add small focused C++ tests that compile with `g++` against the least possible subset of sources. Prefer this for ownership/import/export fixes because it avoids depending on the incomplete Qt GUI port. + +Current local limitation: this NUC has `g++` and `make`, but no `qmake`/Qt development tools installed at the time this file was created. So initial tests should be self-contained command-line tests under `tests/` with a tiny Makefile or shell runner. + +Suggested local commands after adding tests: + +- Focused test: `make -C tests ` +- All local tests: `make -C tests` +- Optional Qt smoke test, only if qmake is available: `qmake wonderbrush2.pro && make -j2` + +## Current hardening focus + +Initial branch focus: ownership/refcount fixes first, then importer/exporter hardening. + +High-priority areas from static review: + +- `Layer::CloneObjects()` and document/resource cloning: avoid extra references after `AddObject()`. +- `CopyCloneContext`: wrap freshly cloned objects with already-owned reference semantics. +- `Layer::RemoveObject()` and `NotifyingList::RemoveObject()`: avoid returning potentially dangling raw pointers. +- Undo/redo edits: avoid unreferenced parent-layer pointers and partial rollback failures. +- SVG and flat-icon importers: replace unsafe fixed-buffer formatting and unchecked parser movement. +- Exporter: restore atomic save behavior before direct overwrite is trusted. diff --git a/src/edits/AddObjectsEdit.h b/src/edits/AddObjectsEdit.h index b8c6c54..7b892ff 100644 --- a/src/edits/AddObjectsEdit.h +++ b/src/edits/AddObjectsEdit.h @@ -67,6 +67,12 @@ class AddObjectsEdit : public UndoableEdit, public Selection::Controller { if (fObjects[i] == NULL) continue; if (!fInsertionLayer->AddObject(fObjects[i], index)) { + for (int32 j = 0; j < i; j++) { + if (fObjects[j] != NULL) + fInsertionLayer->RemoveObject(fObjects[j]); + } + if (fSelection != NULL) + fSelection->DeselectAll(this); fInsertionLayer->SuspendUpdates(false); return B_NO_MEMORY; } diff --git a/src/edits/MoveObjectsEdit.cpp b/src/edits/MoveObjectsEdit.cpp index e47ecc4..0a27deb 100644 --- a/src/edits/MoveObjectsEdit.cpp +++ b/src/edits/MoveObjectsEdit.cpp @@ -19,7 +19,7 @@ MoveObjectsEdit::MoveObjectsEdit(Object** objects, , fObjects(objects) , fObjectCount(objectCount) - , fOldPositions(new(std::nothrow) PositionInfo[objectCount]) + , fOldPositions(new(std::nothrow) PositionInfo[objectCount]()) , fInsertionLayer(insertionLayer) , fInsertionIndex(insertionIndex) @@ -100,13 +100,19 @@ MoveObjectsEdit::InitCheck() } // Check if object tree changes. - PositionInfo newPositions[fObjectCount]; + bool positionsChanged = false; int32 index = fInsertionIndex; for (int32 i = 0; i < fObjectCount; i++) { - newPositions[i].parent = fInsertionLayer; - newPositions[i].index = index++; + if (fObjects[i] == NULL) + continue; + if (fOldPositions[i].parent != fInsertionLayer + || fOldPositions[i].index != index) { + positionsChanged = true; + break; + } + index++; } - if (memcmp(newPositions, fOldPositions, sizeof(newPositions)) == 0) { + if (!positionsChanged) { //printf("MoveObjectsEdit::InitCheck(): no changes!\n"); return B_BAD_VALUE; } @@ -151,6 +157,20 @@ MoveObjectsEdit::Perform(EditContext& context) if (fObjects[i] == NULL) continue; if (!fInsertionLayer->AddObject(fObjects[i], index)) { + for (int32 j = 0; j < i; j++) { + if (fObjects[j] != NULL && fInsertionLayer->HasObject(fObjects[j])) + fInsertionLayer->RemoveObject(fObjects[j]); + } + for (int32 j = 0; j < fObjectCount; j++) { + if (fObjects[j] == NULL || fOldPositions[j].parent == NULL) + continue; + if (!fOldPositions[j].parent->HasObject(fObjects[j])) { + fOldPositions[j].parent->AddObject(fObjects[j], + fOldPositions[j].index); + } + } + if (fSelection != NULL) + fSelection->DeselectAll(this); fInsertionLayer->SuspendUpdates(false); return B_NO_MEMORY; } diff --git a/src/import_export/svg/DocumentBuilder.cpp b/src/import_export/svg/DocumentBuilder.cpp index d8f8bf5..c2ad3e7 100644 --- a/src/import_export/svg/DocumentBuilder.cpp +++ b/src/import_export/svg/DocumentBuilder.cpp @@ -291,7 +291,9 @@ DocumentBuilder::fill_none() void DocumentBuilder::fill_url(const char* url) { - sprintf(cur_attr().fill_url, "%s", url); + snprintf(cur_attr().fill_url, sizeof(cur_attr().fill_url), "%s", + url != NULL ? url : ""); + cur_attr().fill_url[sizeof(cur_attr().fill_url) - 1] = 0; } // stroke_none @@ -305,7 +307,9 @@ DocumentBuilder::stroke_none() void DocumentBuilder::stroke_url(const char* url) { - sprintf(cur_attr().stroke_url, "%s", url); + snprintf(cur_attr().stroke_url, sizeof(cur_attr().stroke_url), "%s", + url != NULL ? url : ""); + cur_attr().stroke_url[sizeof(cur_attr().stroke_url) - 1] = 0; } // opacity diff --git a/src/import_export/svg/DocumentBuilder.h b/src/import_export/svg/DocumentBuilder.h index 6874f61..559d749 100644 --- a/src/import_export/svg/DocumentBuilder.h +++ b/src/import_export/svg/DocumentBuilder.h @@ -110,8 +110,10 @@ struct path_attributes { stroke_width (attr.stroke_width), transform (attr.transform) { - sprintf(stroke_url, "%s", attr.stroke_url); - sprintf(fill_url, "%s", attr.fill_url); + snprintf(stroke_url, sizeof(stroke_url), "%s", attr.stroke_url); + snprintf(fill_url, sizeof(fill_url), "%s", attr.fill_url); + stroke_url[sizeof(stroke_url) - 1] = 0; + fill_url[sizeof(fill_url) - 1] = 0; } // Copy constructor with new index value @@ -128,8 +130,10 @@ struct path_attributes { stroke_width (attr.stroke_width), transform (attr.transform) { - sprintf(stroke_url, "%s", attr.stroke_url); - sprintf(fill_url, "%s", attr.fill_url); + snprintf(stroke_url, sizeof(stroke_url), "%s", attr.stroke_url); + snprintf(fill_url, sizeof(fill_url), "%s", attr.fill_url); + stroke_url[sizeof(stroke_url) - 1] = 0; + fill_url[sizeof(fill_url) - 1] = 0; } }; diff --git a/src/import_export/svg/SVGException.h b/src/import_export/svg/SVGException.h index ce88429..1f85e8c 100644 --- a/src/import_export/svg/SVGException.h +++ b/src/import_export/svg/SVGException.h @@ -45,8 +45,9 @@ class exception { m_msg = new char [4096]; va_list arg; va_start(arg, fmt); - vsprintf(m_msg, fmt, arg); + vsnprintf(m_msg, 4096, fmt, arg); va_end(arg); + m_msg[4095] = 0; } } diff --git a/src/import_export/svg/SVGImporter.cpp b/src/import_export/svg/SVGImporter.cpp index d70503c..a85aa11 100644 --- a/src/import_export/svg/SVGImporter.cpp +++ b/src/import_export/svg/SVGImporter.cpp @@ -74,9 +74,10 @@ SVGImporter::Import(Icon* icon, const entry_ref* ref) ret = builder.GetIcon(icon, this, ref->name); } catch(agg::svg::exception& e) { char error[1024]; - sprintf(error, B_TRANSLATE("Failed to open the file '%s' as " + snprintf(error, sizeof(error), B_TRANSLATE("Failed to open the file '%s' as " "an SVG document.\n\n" "Error: %s"), ref->name, e.msg()); + error[sizeof(error) - 1] = 0; BAlert* alert = new BAlert(B_TRANSLATE("load error"), error, B_TRANSLATE("OK"), NULL, NULL, B_WIDTH_AS_USUAL, B_WARNING_ALERT); diff --git a/src/import_export/svg/SVGParser.cpp b/src/import_export/svg/SVGParser.cpp index e4253dc..ce55bcd 100644 --- a/src/import_export/svg/SVGParser.cpp +++ b/src/import_export/svg/SVGParser.cpp @@ -263,26 +263,31 @@ parse_double(const char* str) } // parse_url -char* -parse_url(const char* str) +bool +parse_url(const char* str, BString& url) { - const char* begin = str; - while (*begin != '#') - begin++; + if (str == NULL) + return false; + const char* begin = strchr(str, '#'); + if (begin == NULL) + return false; begin++; - const char* end = begin; - while (*end != ')') - end++; - end--; + const char* end = strchr(begin, ')'); + if (end == NULL || end <= begin) + return false; - int32 length = end - begin + 2; - char* result = new char[length]; - memcpy(result, begin, length - 1); - result[length - 1] = 0; + // Trim optional whitespace before the closing parenthesis. + while (end > begin && (*(end - 1) == ' ' || *(end - 1) == '\t' + || *(end - 1) == '\n' || *(end - 1) == '\r')) { + end--; + } + if (end <= begin) + return false; - return result; + url.SetTo(begin, end - begin); + return true; } @@ -334,7 +339,8 @@ Parser::parse(const char* pathToFile) FILE* fd = fopen(pathToFile, "r"); if (fd == 0) { - sprintf(msg, "Couldn't open file %s", pathToFile); + snprintf(msg, sizeof(msg), "Couldn't open file %s", pathToFile); + msg[sizeof(msg) - 1] = 0; throw exception(msg); } @@ -343,9 +349,10 @@ Parser::parse(const char* pathToFile) size_t len = fread(fBuffer, 1, buf_size, fd); done = len < buf_size; if (!XML_Parse(p, fBuffer, len, done)) { - sprintf(msg, "%s at line %d\n", + snprintf(msg, sizeof(msg), "%s at line %d\n", XML_ErrorString(XML_GetErrorCode(p)), XML_GetCurrentLineNumber(p)); + msg[sizeof(msg) - 1] = 0; throw exception(msg); } } while (!done); @@ -604,9 +611,9 @@ Parser::parse_attr(const char* name, const char* value) if(strcmp(value, "none") == 0) { fBuilder.fill_none(); } else if (strncmp(value, "url", 3) == 0) { - char* url = parse_url(value); - fBuilder.fill_url(url); - delete[] url; + BString url; + if (parse_url(value, url)) + fBuilder.fill_url(url.String()); } else { fBuilder.fill(parse_color(value)); } @@ -621,9 +628,9 @@ Parser::parse_attr(const char* name, const char* value) if(strcmp(value, "none") == 0) { fBuilder.stroke_none(); } else if (strncmp(value, "url", 3) == 0) { - char* url = parse_url(value); - fBuilder.stroke_url(url); - delete[] url; + BString url; + if (parse_url(value, url)) + fBuilder.stroke_url(url.String()); } else { fBuilder.stroke(parse_color(value)); } diff --git a/src/model/CopyCloneContext.cpp b/src/model/CopyCloneContext.cpp index 8fbcffe..9189e62 100644 --- a/src/model/CopyCloneContext.cpp +++ b/src/model/CopyCloneContext.cpp @@ -20,5 +20,5 @@ CopyCloneContext::~CopyCloneContext() BaseObjectRef CopyCloneContext::Clone(BaseObject* object) { - return BaseObjectRef(object->Clone(*this)); + return BaseObjectRef(object->Clone(*this), true); } diff --git a/src/model/document/Document.cpp b/src/model/document/Document.cpp index e0f0515..557a11d 100644 --- a/src/model/document/Document.cpp +++ b/src/model/document/Document.cpp @@ -10,12 +10,16 @@ #include #include "DocumentSaver.h" +#ifndef WB_DOCUMENT_MINIMAL_TEST #include "DocumentVisitor.h" +#endif #include "EditManager.h" #include "Layer.h" #include "Object.h" +#ifndef WB_DOCUMENT_MINIMAL_TEST #include "Path.h" #include "Style.h" +#endif // constructor @@ -72,11 +76,14 @@ Document::Clone(CloneContext&) const int32 count = source.CountObjects(); for (int32 i = 0; i < count; i++) { BaseObject* object = source.ObjectAtFast(i); - BaseObject* clone = object->Clone(); - if (clone == NULL || !target.AddObject(clone)) { - delete clone; + BaseObject* resourceClone = object->Clone(); + if (resourceClone == NULL) + break; + if (!target.AddObject(resourceClone)) { + resourceClone->RemoveReference(); break; } + resourceClone->RemoveReference(); } // Implement a ResourceResolver that will mapped from the @@ -208,6 +215,7 @@ Document::SetExportSaver(DocumentSaver* saver) // #pragma mark - +#ifndef WB_DOCUMENT_MINIMAL_TEST class Indentation { public: Indentation() @@ -316,6 +324,12 @@ Document::PrintToStream() { PrintVisitor visitor(this); } +#else +void +Document::PrintToStream() +{ +} +#endif // #pragma mark - diff --git a/src/model/document/NotifyingList.h b/src/model/document/NotifyingList.h index 7511bb4..015a6d8 100644 --- a/src/model/document/NotifyingList.h +++ b/src/model/document/NotifyingList.h @@ -122,16 +122,16 @@ class NotifyingList { return false; } - ObjectType* RemoveObject(int32 index) + Reference RemoveObject(int32 index) { ObjectType* object = reinterpret_cast( fObjects.RemoveItem(index)); if (object) { _NotifyObjectRemoved(object, index); - object->RemoveReference(); + return Reference(object, true); } - return object; + return Reference(); } void MakeEmpty() diff --git a/src/model/objects/Layer.cpp b/src/model/objects/Layer.cpp index 88ab84d..1a9de63 100644 --- a/src/model/objects/Layer.cpp +++ b/src/model/objects/Layer.cpp @@ -279,7 +279,10 @@ bool Layer::AddObject(Object* object, int32 index) { //printf("%p->Layer::AddObject(%p, %ld)\n", this, object, index); - if (object && fObjects.AddItem(object, index)) { + if (object == NULL || fObjects.HasItem(object)) + return false; + + if (fObjects.AddItem(object, index)) { object->AddReference(); BList listeners(fListeners); @@ -304,7 +307,7 @@ Layer::AddObject(Object* object, int32 index) } // RemoveObject -Object* +Reference Layer::RemoveObject(int32 index) { Object* object = ObjectAt(index); @@ -323,7 +326,7 @@ Layer::RemoveObject(int32 index) if (fObjects.RemoveItem(index) == NULL) { // Should not happen, but roll back in any case. object->SetParent(this); - return NULL; + return Reference(); } BList listeners(fListeners); @@ -334,16 +337,16 @@ Layer::RemoveObject(int32 index) } Invalidate(invalidArea, index); - object->RemoveReference(); + return Reference(object, true); } - return object; + return Reference(); } // RemoveObject bool Layer::RemoveObject(Object* object) { - return RemoveObject(IndexOf(object)) != NULL; + return RemoveObject(IndexOf(object)).Get() != NULL; } // ObjectAt @@ -389,10 +392,13 @@ Layer::CloneObjects(Layer* target, CloneContext& context) const for (int32 i = 0; i < count; i++) { Object* object = ObjectAtFast(i); Object* clone = (Object*)object->Clone(context); - if (clone == NULL || !target->AddObject(clone)) { - delete clone; + if (clone == NULL) + break; + if (!target->AddObject(clone)) { + clone->RemoveReference(); break; } + clone->RemoveReference(); } } diff --git a/src/model/objects/Layer.h b/src/model/objects/Layer.h index 953247c..36f5d9e 100644 --- a/src/model/objects/Layer.h +++ b/src/model/objects/Layer.h @@ -65,7 +65,7 @@ class Layer : public Object { // Layer bool AddObject(Object* object); bool AddObject(Object* object, int32 index); - Object* RemoveObject(int32 index); + Reference RemoveObject(int32 index); bool RemoveObject(Object* object); Object* ObjectAt(int32 index) const; Object* ObjectAtFast(int32 index) const; diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..b6d4416 --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,128 @@ +CXX ?= g++ +CXXFLAGS ?= -std=c++11 -Wall -Wextra -Werror -g + +ROOT := .. +PLATFORM := $(ROOT)/src/platform/qt/system +COMMON_INCLUDES := \ + -I$(ROOT)/tests/support \ + -I$(ROOT)/tests/stubs \ + -I$(ROOT)/src/support \ + -I$(ROOT)/src/agg/include \ + -I$(PLATFORM)/include \ + -I$(PLATFORM) + +SUPPORT_SOURCES := \ + $(ROOT)/src/support/Referenceable.cpp + +BASE_OBJECT_STUB_SOURCES := \ + $(ROOT)/tests/support/BaseObjectStubs.cpp \ + $(SUPPORT_SOURCES) + +.PHONY: all build ownership-referenceable ownership-copy-clone-context ownership-layer-clone-objects ownership-document-clone-resources edits-move-objects import-export-svg-error-formatting import-export-svg-url-parsing clean + +all: ownership-referenceable ownership-copy-clone-context ownership-layer-clone-objects ownership-document-clone-resources edits-move-objects import-export-svg-error-formatting import-export-svg-url-parsing + +build: + mkdir -p build/ownership build/edits build/import_export + +ownership-referenceable: build + $(CXX) $(CXXFLAGS) $(COMMON_INCLUDES) \ + $(ROOT)/tests/ownership/ReferenceableLifecycleTest.cpp \ + $(SUPPORT_SOURCES) \ + -o build/ownership/referenceable_lifecycle + ./build/ownership/referenceable_lifecycle + +ownership-copy-clone-context: build + $(CXX) $(CXXFLAGS) $(COMMON_INCLUDES) \ + -I$(ROOT)/src/model \ + -I$(ROOT)/src/model/property \ + $(ROOT)/tests/ownership/CopyCloneContextTest.cpp \ + $(ROOT)/src/model/CloneContext.cpp \ + $(ROOT)/src/model/CopyCloneContext.cpp \ + $(BASE_OBJECT_STUB_SOURCES) \ + -o build/ownership/copy_clone_context + ./build/ownership/copy_clone_context + +ownership-layer-clone-objects: build + $(CXX) $(CXXFLAGS) -Wno-unused-parameter -Wno-multichar -include $(ROOT)/tests/stubs/TestCompat.h $(COMMON_INCLUDES) \ + -I$(ROOT)/src/model \ + -I$(ROOT)/src/model/document \ + -I$(ROOT)/src/model/objects \ + -I$(ROOT)/src/model/fills \ + -I$(ROOT)/src/model/property \ + -I$(ROOT)/src/model/property/specific_properties \ + -I$(ROOT)/src/model/rendering \ + -I$(ROOT)/src/model/snapshots \ + $(ROOT)/tests/ownership/LayerCloneObjectsTest.cpp \ + $(ROOT)/src/model/CloneContext.cpp \ + $(ROOT)/src/model/objects/Object.cpp \ + $(ROOT)/src/model/objects/Layer.cpp \ + $(ROOT)/tests/support/TransformableStubs.cpp \ + $(ROOT)/tests/support/LayerDependencyStubs.cpp \ + $(BASE_OBJECT_STUB_SOURCES) \ + -o build/ownership/layer_clone_objects + ./build/ownership/layer_clone_objects + +ownership-document-clone-resources: build + $(CXX) $(CXXFLAGS) -Wno-unused-parameter -Wno-multichar -DWB_DOCUMENT_MINIMAL_TEST -include $(ROOT)/tests/stubs/TestCompat.h $(COMMON_INCLUDES) \ + -I$(ROOT)/src/model \ + -I$(ROOT)/src/model/document \ + -I$(ROOT)/src/savers \ + -I$(ROOT)/src/model/objects \ + -I$(ROOT)/src/model/fills \ + -I$(ROOT)/src/model/property \ + -I$(ROOT)/src/model/property/specific_properties \ + -I$(ROOT)/src/model/rendering \ + -I$(ROOT)/src/model/snapshots \ + -I$(ROOT)/src/edits/base \ + $(ROOT)/tests/ownership/DocumentCloneResourcesTest.cpp \ + $(ROOT)/src/model/CloneContext.cpp \ + $(ROOT)/src/model/document/Document.cpp \ + $(ROOT)/src/model/objects/Object.cpp \ + $(ROOT)/src/model/objects/Layer.cpp \ + $(ROOT)/tests/support/TransformableStubs.cpp \ + $(ROOT)/tests/support/LayerDependencyStubs.cpp \ + $(BASE_OBJECT_STUB_SOURCES) \ + -o build/ownership/document_clone_resources + ./build/ownership/document_clone_resources + +edits-move-objects: build + $(CXX) $(CXXFLAGS) -Wno-unused-parameter -Wno-multichar -include $(ROOT)/tests/stubs/TestCompat.h $(COMMON_INCLUDES) \ + -I$(ROOT)/src/model \ + -I$(ROOT)/src/model/document \ + -I$(ROOT)/src/model/objects \ + -I$(ROOT)/src/model/fills \ + -I$(ROOT)/src/model/property \ + -I$(ROOT)/src/model/property/specific_properties \ + -I$(ROOT)/src/model/rendering \ + -I$(ROOT)/src/model/snapshots \ + -I$(ROOT)/src/edits \ + -I$(ROOT)/src/edits/base \ + $(ROOT)/tests/edits/MoveObjectsEditTest.cpp \ + $(ROOT)/src/model/CloneContext.cpp \ + $(ROOT)/src/model/objects/Object.cpp \ + $(ROOT)/src/model/objects/Layer.cpp \ + $(ROOT)/src/edits/MoveObjectsEdit.cpp \ + $(ROOT)/src/model/Selection.cpp \ + $(ROOT)/src/model/Selectable.cpp \ + $(ROOT)/tests/support/TransformableStubs.cpp \ + $(ROOT)/tests/support/LayerDependencyStubs.cpp \ + $(BASE_OBJECT_STUB_SOURCES) \ + -o build/edits/move_objects + ./build/edits/move_objects + +import-export-svg-error-formatting: build + $(CXX) $(CXXFLAGS) -fsanitize=address $(COMMON_INCLUDES) \ + -I$(ROOT)/src/import_export/svg \ + $(ROOT)/tests/import_export/SVGErrorFormattingTest.cpp \ + -o build/import_export/svg_error_formatting + ./build/import_export/svg_error_formatting + +import-export-svg-url-parsing: build + $(CXX) $(CXXFLAGS) $(COMMON_INCLUDES) \ + $(ROOT)/tests/import_export/SVGUrlParsingTest.cpp \ + -o build/import_export/svg_url_parsing + ./build/import_export/svg_url_parsing + +clean: + rm -rf build diff --git a/tests/edits/MoveObjectsEditTest.cpp b/tests/edits/MoveObjectsEditTest.cpp new file mode 100644 index 0000000..88e093b --- /dev/null +++ b/tests/edits/MoveObjectsEditTest.cpp @@ -0,0 +1,151 @@ +#include "Test.h" + +#include "Layer.h" +#include "AddObjectsEdit.h" +#include "MoveObjectsEdit.h" + +#include +#include + +// Edit-specific test seams that are not provided by the shared model stubs. +UndoableEdit::UndoableEdit() + : Referenceable() + , fTimeStamp(0) +{ +} + +UndoableEdit::~UndoableEdit() {} +status_t UndoableEdit::InitCheck() { return B_OK; } +status_t UndoableEdit::Perform(EditContext&) { return B_OK; } +status_t UndoableEdit::Undo(EditContext&) { return B_OK; } +status_t UndoableEdit::Redo(EditContext& context) { return Perform(context); } +void UndoableEdit::GetName(BString&) {} +bool UndoableEdit::UndoesPrevious(const UndoableEdit*) { return false; } +bool UndoableEdit::CombineWithNext(const UndoableEdit*) { return false; } +bool UndoableEdit::CombineWithPrevious(const UndoableEdit*) { return false; } + +class EditContext { +}; + +class CountedObject : public Object { +public: + CountedObject(int* destructorCount) + : Object() + , fDestructorCount(destructorCount) + { + } + + ~CountedObject() override + { + (*fDestructorCount)++; + } + + ObjectSnapshot* Snapshot() const override + { + return 0; + } + + BaseObject* Clone(CloneContext&) const override + { + return new CountedObject(fDestructorCount); + } + + const char* DefaultName() const override + { + return "CountedObject"; + } + +private: + int* fDestructorCount; +}; + +static void +TestAddObjectsEditRollsBackPartialInsertionFailure() +{ + int destructorCount = 0; + + { + Layer layer(BRect(0, 0, 100, 100)); + CountedObject* object = new CountedObject(&destructorCount); + + Object** objects = new Object*[2]; + objects[0] = object; + objects[1] = object; + + { + AddObjectsEdit edit(objects, 2, &layer, 0, 0); + ASSERT_TRUE(!object->RemoveReference()); + + EditContext context; + ASSERT_EQ(B_NO_MEMORY, edit.Perform(context)); + ASSERT_EQ(0, layer.CountObjects()); + } + + ASSERT_EQ(1, destructorCount); + } +} + +static void +TestMoveObjectsEditRestoresSourceOnPartialInsertionFailure() +{ + int destructorCount = 0; + + { + Layer source(BRect(0, 0, 100, 100)); + Layer target(BRect(0, 0, 100, 100)); + CountedObject* object = new CountedObject(&destructorCount); + ASSERT_TRUE(source.AddObject(object)); + ASSERT_TRUE(!object->RemoveReference()); + + Object** objects = new Object*[2]; + objects[0] = object; + objects[1] = object; + + { + MoveObjectsEdit edit(objects, 2, &target, 0, 0); + EditContext context; + ASSERT_EQ(B_NO_MEMORY, edit.Perform(context)); + ASSERT_EQ(1, source.CountObjects()); + ASSERT_TRUE(source.ObjectAt(0) == object); + ASSERT_EQ(0, target.CountObjects()); + } + } + + ASSERT_EQ(1, destructorCount); +} + +static void +TestNestedSelectionNoOpMoveIsRejected() +{ + int childDestructorCount = 0; + + { + Layer root(BRect(0, 0, 100, 100)); + Layer* childLayer = new Layer(BRect(0, 0, 50, 50)); + CountedObject* child = new CountedObject(&childDestructorCount); + + ASSERT_TRUE(root.AddObject(childLayer)); + ASSERT_TRUE(!childLayer->RemoveReference()); + ASSERT_TRUE(childLayer->AddObject(child)); + ASSERT_TRUE(!child->RemoveReference()); + + Object** objects = new Object*[2]; + objects[0] = childLayer; + objects[1] = child; + + MoveObjectsEdit edit(objects, 2, &root, 0, 0); + ASSERT_EQ(B_BAD_VALUE, edit.InitCheck()); + } + + ASSERT_EQ(1, childDestructorCount); +} + +int +main() +{ + TestAddObjectsEditRollsBackPartialInsertionFailure(); + TestMoveObjectsEditRestoresSourceOnPartialInsertionFailure(); + TestNestedSelectionNoOpMoveIsRejected(); + printf("edits-move-objects: PASS\n"); + return 0; +} diff --git a/tests/import_export/SVGErrorFormattingTest.cpp b/tests/import_export/SVGErrorFormattingTest.cpp new file mode 100644 index 0000000..a31440a --- /dev/null +++ b/tests/import_export/SVGErrorFormattingTest.cpp @@ -0,0 +1,25 @@ +#include "Test.h" + +#include "SVGException.h" + +#include +#include + +static void +TestSVGExceptionFormattingIsBounded() +{ + std::string longText(8192, 'x'); + agg::svg::exception exception("prefix:%s", longText.c_str()); + + ASSERT_TRUE(exception.msg() != 0); + ASSERT_TRUE(strncmp(exception.msg(), "prefix:", 7) == 0); + ASSERT_TRUE(strlen(exception.msg()) < 4096); +} + +int +main() +{ + TestSVGExceptionFormattingIsBounded(); + printf("import-export-svg-error-formatting: PASS\n"); + return 0; +} diff --git a/tests/import_export/SVGUrlParsingTest.cpp b/tests/import_export/SVGUrlParsingTest.cpp new file mode 100644 index 0000000..924c3c0 --- /dev/null +++ b/tests/import_export/SVGUrlParsingTest.cpp @@ -0,0 +1,46 @@ +#include "Test.h" + +#include +#include +#include + +static std::string +ReadFile(const char* path) +{ + std::ifstream file(path); + ASSERT_TRUE(file.good()); + std::ostringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); +} + +static void +TestSVGUrlParsingDoesNotScanPastStringEnd() +{ + std::string parser = ReadFile("../src/import_export/svg/SVGParser.cpp"); + + ASSERT_TRUE(parser.find("while (*begin != '#')") == std::string::npos); + ASSERT_TRUE(parser.find("while (*end != ')')") == std::string::npos); + ASSERT_TRUE(parser.find("parse_url(value)") == std::string::npos); +} + +static void +TestSVGUrlStorageUsesBoundedCopies() +{ + std::string builder = ReadFile("../src/import_export/svg/DocumentBuilder.cpp"); + std::string builderHeader = ReadFile("../src/import_export/svg/DocumentBuilder.h"); + + ASSERT_TRUE(builder.find("sprintf(cur_attr().fill_url") == std::string::npos); + ASSERT_TRUE(builder.find("sprintf(cur_attr().stroke_url") == std::string::npos); + ASSERT_TRUE(builderHeader.find("sprintf(stroke_url") == std::string::npos); + ASSERT_TRUE(builderHeader.find("sprintf(fill_url") == std::string::npos); +} + +int +main() +{ + TestSVGUrlParsingDoesNotScanPastStringEnd(); + TestSVGUrlStorageUsesBoundedCopies(); + printf("import-export-svg-url-parsing: PASS\n"); + return 0; +} diff --git a/tests/ownership/CopyCloneContextTest.cpp b/tests/ownership/CopyCloneContextTest.cpp new file mode 100644 index 0000000..dbcbba0 --- /dev/null +++ b/tests/ownership/CopyCloneContextTest.cpp @@ -0,0 +1,64 @@ +#include "Test.h" + +#include "CopyCloneContext.h" + +#include + +class CountedBaseObject : public BaseObject { +public: + CountedBaseObject(int* destructorCount, int* cloneDestructorCount) + : BaseObject() + , fDestructorCount(destructorCount) + , fCloneDestructorCount(cloneDestructorCount) + { + } + + ~CountedBaseObject() override + { + (*fDestructorCount)++; + } + + BaseObject* Clone(CloneContext&) const override + { + return new CountedBaseObject(fCloneDestructorCount, fCloneDestructorCount); + } + + const char* DefaultName() const override + { + return "CountedBaseObject"; + } + +private: + int* fDestructorCount; + int* fCloneDestructorCount; +}; + +static void +TestCopyCloneContextAdoptsFreshCloneReference() +{ + int originalDestructorCount = 0; + int cloneDestructorCount = 0; + + CountedBaseObject* original = new CountedBaseObject(&originalDestructorCount, + &cloneDestructorCount); + + { + CopyCloneContext context; + BaseObjectRef clone = context.Clone(original); + ASSERT_TRUE(clone.Get() != 0); + ASSERT_EQ(1, clone->CountReferences()); + } + + ASSERT_EQ(1, cloneDestructorCount); + ASSERT_EQ(0, originalDestructorCount); + ASSERT_TRUE(original->RemoveReference()); + ASSERT_EQ(1, originalDestructorCount); +} + +int +main() +{ + TestCopyCloneContextAdoptsFreshCloneReference(); + printf("ownership-copy-clone-context: PASS\n"); + return 0; +} diff --git a/tests/ownership/DocumentCloneResourcesTest.cpp b/tests/ownership/DocumentCloneResourcesTest.cpp new file mode 100644 index 0000000..28e4fab --- /dev/null +++ b/tests/ownership/DocumentCloneResourcesTest.cpp @@ -0,0 +1,72 @@ +#include "Test.h" + +#include "Document.h" +#include "CloneContext.h" + +#include + +class CountedResource : public BaseObject { +public: + CountedResource(int* destructorCount, int* cloneDestructorCount) + : BaseObject() + , fDestructorCount(destructorCount) + , fCloneDestructorCount(cloneDestructorCount) + { + } + + ~CountedResource() override + { + (*fDestructorCount)++; + } + + BaseObject* Clone(CloneContext&) const override + { + return new CountedResource(fCloneDestructorCount, fCloneDestructorCount); + } + + const char* DefaultName() const override + { + return "CountedResource"; + } + +private: + int* fDestructorCount; + int* fCloneDestructorCount; +}; + +static void +TestDocumentCloneReleasesFreshGlobalResourceReferenceAfterInsertion() +{ + int originalDestructorCount = 0; + int cloneDestructorCount = 0; + + { + Document document(BRect(0, 0, 10, 10)); + CountedResource* original = new CountedResource(&originalDestructorCount, + &cloneDestructorCount); + ASSERT_TRUE(document.GlobalResources().AddObject(original)); + ASSERT_EQ(2, original->CountReferences()); + ASSERT_TRUE(!original->RemoveReference()); + ASSERT_EQ(1, original->CountReferences()); + + CloneContext context; + Document* clonedDocument = dynamic_cast(document.Clone(context)); + ASSERT_TRUE(clonedDocument != 0); + ASSERT_EQ(1, clonedDocument->CountReferences()); + ASSERT_EQ(0, cloneDestructorCount); + + ASSERT_TRUE(clonedDocument->RemoveReference()); + ASSERT_EQ(1, cloneDestructorCount); + ASSERT_EQ(0, originalDestructorCount); + } + + ASSERT_EQ(1, originalDestructorCount); +} + +int +main() +{ + TestDocumentCloneReleasesFreshGlobalResourceReferenceAfterInsertion(); + printf("ownership-document-clone-resources: PASS\n"); + return 0; +} diff --git a/tests/ownership/LayerCloneObjectsTest.cpp b/tests/ownership/LayerCloneObjectsTest.cpp new file mode 100644 index 0000000..33796ce --- /dev/null +++ b/tests/ownership/LayerCloneObjectsTest.cpp @@ -0,0 +1,147 @@ +#include "Test.h" + +#include "Layer.h" +#include "NotifyingList.h" + +#include +#include +#include + +class CountedObject : public Object { +public: + CountedObject(int* destructorCount, int* cloneDestructorCount) + : Object() + , fDestructorCount(destructorCount) + , fCloneDestructorCount(cloneDestructorCount) + { + } + + ~CountedObject() override + { + (*fDestructorCount)++; + } + + ObjectSnapshot* Snapshot() const override + { + return 0; + } + + BaseObject* Clone(CloneContext&) const override + { + return new CountedObject(fCloneDestructorCount, fCloneDestructorCount); + } + + const char* DefaultName() const override + { + return "CountedObject"; + } + +private: + int* fDestructorCount; + int* fCloneDestructorCount; +}; + +static void +TestCloneObjectsReleasesFreshCloneReferenceAfterInsertion() +{ + int originalDestructorCount = 0; + int cloneDestructorCount = 0; + + { + Layer source(BRect(0, 0, 10, 10)); + CountedObject* original = new CountedObject(&originalDestructorCount, + &cloneDestructorCount); + ASSERT_TRUE(source.AddObject(original)); + ASSERT_EQ(2, original->CountReferences()); + ASSERT_TRUE(!original->RemoveReference()); + ASSERT_EQ(1, original->CountReferences()); + + CloneContext context; + Layer* clonedLayer = dynamic_cast(source.Clone(context)); + ASSERT_TRUE(clonedLayer != 0); + ASSERT_EQ(1, clonedLayer->CountReferences()); + ASSERT_EQ(0, cloneDestructorCount); + + ASSERT_TRUE(clonedLayer->RemoveReference()); + ASSERT_EQ(1, cloneDestructorCount); + ASSERT_EQ(0, originalDestructorCount); + } + + ASSERT_EQ(1, originalDestructorCount); +} + +static void +TestLayerRejectsDuplicateObjectInsertion() +{ + int destructorCount = 0; + int cloneDestructorCount = 0; + + { + Layer layer(BRect(0, 0, 10, 10)); + CountedObject* object = new CountedObject(&destructorCount, + &cloneDestructorCount); + + ASSERT_TRUE(layer.AddObject(object)); + ASSERT_EQ(1, layer.CountObjects()); + ASSERT_TRUE(!object->RemoveReference()); + ASSERT_EQ(1, object->CountReferences()); + + ASSERT_TRUE(!layer.AddObject(object)); + ASSERT_EQ(1, layer.CountObjects()); + ASSERT_EQ(1, object->CountReferences()); + } + + ASSERT_EQ(1, destructorCount); +} + +static void +TestRemoveObjectIndexAPIsDoNotReturnReleasedPointers() +{ + static_assert(std::is_same().RemoveObject(0)), + Reference >::value, + "Layer::RemoveObject(index) must return a reference that keeps the removed object alive"); + static_assert(std::is_same&>().RemoveObject(0)), + Reference >::value, + "NotifyingList::RemoveObject(index) must return a reference that keeps the removed object alive"); + + int layerDestructorCount = 0; + int listDestructorCount = 0; + int cloneDestructorCount = 0; + { + Layer layer(BRect(0, 0, 10, 10)); + CountedObject* object = new CountedObject(&layerDestructorCount, + &cloneDestructorCount); + ASSERT_TRUE(layer.AddObject(object)); + ASSERT_TRUE(!object->RemoveReference()); + Reference removed = layer.RemoveObject(0); + ASSERT_TRUE(removed.Get() == object); + ASSERT_EQ(0, layerDestructorCount); + removed.Unset(); + ASSERT_EQ(1, layerDestructorCount); + } + ASSERT_EQ(1, layerDestructorCount); + + { + NotifyingList list; + CountedObject* object = new CountedObject(&listDestructorCount, + &cloneDestructorCount); + ASSERT_TRUE(list.AddObject(object)); + ASSERT_TRUE(!object->RemoveReference()); + Reference removed = list.RemoveObject(0); + ASSERT_TRUE(removed.Get() == object); + ASSERT_EQ(0, listDestructorCount); + removed.Unset(); + ASSERT_EQ(1, listDestructorCount); + } + ASSERT_EQ(1, listDestructorCount); +} + +int +main() +{ + TestCloneObjectsReleasesFreshCloneReferenceAfterInsertion(); + TestLayerRejectsDuplicateObjectInsertion(); + TestRemoveObjectIndexAPIsDoNotReturnReleasedPointers(); + printf("ownership-layer-clone-objects: PASS\n"); + return 0; +} diff --git a/tests/ownership/ReferenceableLifecycleTest.cpp b/tests/ownership/ReferenceableLifecycleTest.cpp new file mode 100644 index 0000000..05f7ef8 --- /dev/null +++ b/tests/ownership/ReferenceableLifecycleTest.cpp @@ -0,0 +1,72 @@ +#include "Test.h" + +#include "Referenceable.h" + +class CountedReferenceable : public Referenceable { +public: + CountedReferenceable(int* destructorCount) + : Referenceable(true) + , fDestructorCount(destructorCount) + { + } + + ~CountedReferenceable() override + { + (*fDestructorCount)++; + } + +private: + int* fDestructorCount; +}; + +static void +TestInitialReferenceAndRemovalDeletesObject() +{ + int destructorCount = 0; + CountedReferenceable* object = new CountedReferenceable(&destructorCount); + + ASSERT_EQ(1, object->CountReferences()); + ASSERT_TRUE(object->RemoveReference()); + ASSERT_EQ(1, destructorCount); +} + +static void +TestReferenceAdoptsExistingReferenceWithoutIncrementing() +{ + int destructorCount = 0; + CountedReferenceable* object = new CountedReferenceable(&destructorCount); + + { + Reference reference(object, true); + ASSERT_EQ(1, reference->CountReferences()); + } + + ASSERT_EQ(1, destructorCount); +} + +static void +TestReferenceAddsReferenceWhenNotAdopting() +{ + int destructorCount = 0; + CountedReferenceable* object = new CountedReferenceable(&destructorCount); + + { + Reference reference(object); + ASSERT_EQ(2, reference->CountReferences()); + } + + ASSERT_EQ(1, object->CountReferences()); + ASSERT_EQ(0, destructorCount); + ASSERT_TRUE(object->RemoveReference()); + ASSERT_EQ(1, destructorCount); +} + +int +main() +{ + TestInitialReferenceAndRemovalDeletesObject(); + TestReferenceAdoptsExistingReferenceWithoutIncrementing(); + TestReferenceAddsReferenceWhenNotAdopting(); + printf("ownership-referenceable: PASS\n"); + return 0; +} diff --git a/tests/stubs/AbstractLOAdapter.h b/tests/stubs/AbstractLOAdapter.h new file mode 100644 index 0000000..a2fce8e --- /dev/null +++ b/tests/stubs/AbstractLOAdapter.h @@ -0,0 +1,14 @@ +#ifndef WB_TEST_STUB_ABSTRACT_LO_ADAPTER_H +#define WB_TEST_STUB_ABSTRACT_LO_ADAPTER_H + +class BHandler; +class BMessage; + +class AbstractLOAdapter { +public: + AbstractLOAdapter(BHandler*) {} + virtual ~AbstractLOAdapter() {} + void DeliverMessage(BMessage&) {} +}; + +#endif // WB_TEST_STUB_ABSTRACT_LO_ADAPTER_H diff --git a/tests/stubs/Autolock.h b/tests/stubs/Autolock.h new file mode 100644 index 0000000..e5f9731 --- /dev/null +++ b/tests/stubs/Autolock.h @@ -0,0 +1,31 @@ +#ifndef WB_TEST_STUB_AUTOLOCK_H +#define WB_TEST_STUB_AUTOLOCK_H + +#include + +class BAutolock { +public: + BAutolock(BLocker* locker) + : fLocker(locker) + { + if (fLocker != 0) + fLocker->Lock(); + } + + BAutolock(BLocker& locker) + : fLocker(&locker) + { + fLocker->Lock(); + } + + ~BAutolock() + { + if (fLocker != 0) + fLocker->Unlock(); + } + +private: + BLocker* fLocker; +}; + +#endif // WB_TEST_STUB_AUTOLOCK_H diff --git a/tests/stubs/Bitmap.h b/tests/stubs/Bitmap.h new file mode 100644 index 0000000..99540e9 --- /dev/null +++ b/tests/stubs/Bitmap.h @@ -0,0 +1,7 @@ +#ifndef WB_TEST_STUB_BITMAP_H +#define WB_TEST_STUB_BITMAP_H + +class BBitmap { +}; + +#endif // WB_TEST_STUB_BITMAP_H diff --git a/tests/stubs/CommonPropertyIDs.h b/tests/stubs/CommonPropertyIDs.h new file mode 100644 index 0000000..230b7fe --- /dev/null +++ b/tests/stubs/CommonPropertyIDs.h @@ -0,0 +1,10 @@ +#ifndef WB_TEST_STUB_COMMON_PROPERTY_IDS_H +#define WB_TEST_STUB_COMMON_PROPERTY_IDS_H + +enum { + PROPERTY_NAME = 1, + PROPERTY_OPACITY = 2, + PROPERTY_BLENDING_MODE = 3 +}; + +#endif // WB_TEST_STUB_COMMON_PROPERTY_IDS_H diff --git a/tests/stubs/EditManager.h b/tests/stubs/EditManager.h new file mode 100644 index 0000000..ec52ab9 --- /dev/null +++ b/tests/stubs/EditManager.h @@ -0,0 +1,12 @@ +#ifndef WB_TEST_STUB_EDIT_MANAGER_H +#define WB_TEST_STUB_EDIT_MANAGER_H + +class Document; + +class EditManager { +public: + EditManager(Document*) {} + ~EditManager() {} +}; + +#endif // WB_TEST_STUB_EDIT_MANAGER_H diff --git a/tests/stubs/LayoutContext.h b/tests/stubs/LayoutContext.h new file mode 100644 index 0000000..53e6dd9 --- /dev/null +++ b/tests/stubs/LayoutContext.h @@ -0,0 +1,7 @@ +#ifndef WB_TEST_STUB_LAYOUT_CONTEXT_H +#define WB_TEST_STUB_LAYOUT_CONTEXT_H + +class LayoutContext { +}; + +#endif // WB_TEST_STUB_LAYOUT_CONTEXT_H diff --git a/tests/stubs/LayoutState.h b/tests/stubs/LayoutState.h new file mode 100644 index 0000000..ab75d5b --- /dev/null +++ b/tests/stubs/LayoutState.h @@ -0,0 +1,7 @@ +#ifndef WB_TEST_STUB_LAYOUT_STATE_H +#define WB_TEST_STUB_LAYOUT_STATE_H + +class LayoutState { +}; + +#endif // WB_TEST_STUB_LAYOUT_STATE_H diff --git a/tests/stubs/List.h b/tests/stubs/List.h new file mode 100644 index 0000000..3de1742 --- /dev/null +++ b/tests/stubs/List.h @@ -0,0 +1,83 @@ +#ifndef WB_TEST_STUB_LIST_H +#define WB_TEST_STUB_LIST_H + +#include + +#include + +template +class List { +public: + List() {} +}; + +class BList { +public: + BList(int32 = 20) {} + BList(const BList& other) + : fItems(other.fItems) + { + } + ~BList() {} + + bool AddItem(void* item) + { + fItems.push_back(item); + return true; + } + + bool AddItem(void* item, int32 index) + { + if (index < 0 || index > (int32)fItems.size()) + return false; + fItems.insert(fItems.begin() + index, item); + return true; + } + + bool RemoveItem(void* item) + { + int32 index = IndexOf(item); + if (index < 0) + return false; + fItems.erase(fItems.begin() + index); + return true; + } + + void* RemoveItem(int32 index) + { + if (index < 0 || index >= (int32)fItems.size()) + return 0; + void* item = fItems[index]; + fItems.erase(fItems.begin() + index); + return item; + } + + bool HasItem(void* item) const { return IndexOf(item) >= 0; } + bool HasItem(const void* item) const { return IndexOf(item) >= 0; } + + int32 IndexOf(void* item) const { return IndexOf((const void*)item); } + int32 IndexOf(const void* item) const + { + for (size_t i = 0; i < fItems.size(); i++) { + if (fItems[i] == item) + return (int32)i; + } + return -1; + } + + void* ItemAt(int32 index) const + { + if (index < 0 || index >= (int32)fItems.size()) + return 0; + return fItems[index]; + } + + void* ItemAtFast(int32 index) const { return fItems[index]; } + int32 CountItems() const { return (int32)fItems.size(); } + void MakeEmpty() { fItems.clear(); } + +private: + std::vector fItems; +}; + +#endif // WB_TEST_STUB_LIST_H diff --git a/tests/stubs/Locker.h b/tests/stubs/Locker.h new file mode 100644 index 0000000..b308a0c --- /dev/null +++ b/tests/stubs/Locker.h @@ -0,0 +1,11 @@ +#ifndef WB_TEST_STUB_LOCKER_H +#define WB_TEST_STUB_LOCKER_H + +class BLocker { +public: + BLocker() {} + bool Lock() { return true; } + void Unlock() {} +}; + +#endif // WB_TEST_STUB_LOCKER_H diff --git a/tests/stubs/Message.h b/tests/stubs/Message.h new file mode 100644 index 0000000..8842301 --- /dev/null +++ b/tests/stubs/Message.h @@ -0,0 +1,14 @@ +#ifndef WB_TEST_STUB_MESSAGE_H +#define WB_TEST_STUB_MESSAGE_H + +#include + +class BMessage { +public: + status_t FindString(const char*, const char**) const { return B_ERROR; } + status_t AddString(const char*, const char*) { return B_OK; } + status_t AddPointer(const char*, const void*) { return B_OK; } + status_t AddInt32(const char*, int32) { return B_OK; } +}; + +#endif // WB_TEST_STUB_MESSAGE_H diff --git a/tests/stubs/OptionProperty.h b/tests/stubs/OptionProperty.h new file mode 100644 index 0000000..21ce71c --- /dev/null +++ b/tests/stubs/OptionProperty.h @@ -0,0 +1,26 @@ +#ifndef WB_TEST_STUB_OPTION_PROPERTY_H +#define WB_TEST_STUB_OPTION_PROPERTY_H + +#include "Property.h" + +class OptionProperty : public Property { +public: + OptionProperty(uint32 identifier) + : Property(identifier) + , fCurrentOptionID(0) + { + } + + void AddOption(int32, const char*) {} + int32 CurrentOptionID() const { return fCurrentOptionID; } + bool SetCurrentOptionID(int32 id) + { + fCurrentOptionID = id; + return true; + } + +private: + int32 fCurrentOptionID; +}; + +#endif // WB_TEST_STUB_OPTION_PROPERTY_H diff --git a/tests/stubs/Point.h b/tests/stubs/Point.h new file mode 100644 index 0000000..cd0a738 --- /dev/null +++ b/tests/stubs/Point.h @@ -0,0 +1,22 @@ +#ifndef WB_TEST_STUB_POINT_H +#define WB_TEST_STUB_POINT_H + +class BPoint { +public: + BPoint() + : x(0) + , y(0) + { + } + + BPoint(float xValue, float yValue) + : x(xValue) + , y(yValue) + { + } + + float x; + float y; +}; + +#endif // WB_TEST_STUB_POINT_H diff --git a/tests/stubs/Property.h b/tests/stubs/Property.h new file mode 100644 index 0000000..64de0d8 --- /dev/null +++ b/tests/stubs/Property.h @@ -0,0 +1,23 @@ +#ifndef WB_TEST_STUB_PROPERTY_H +#define WB_TEST_STUB_PROPERTY_H + +#include + +class PropertyObject; +class BString; + +class Property { +public: + Property(uint32 = 0) {} + virtual ~Property() {} +}; + +class IntProperty : public Property { +public: + IntProperty(uint32 identifier, int32, int32, int32) + : Property(identifier) + { + } +}; + +#endif // WB_TEST_STUB_PROPERTY_H diff --git a/tests/stubs/PropertyObject.h b/tests/stubs/PropertyObject.h new file mode 100644 index 0000000..1fc043f --- /dev/null +++ b/tests/stubs/PropertyObject.h @@ -0,0 +1,15 @@ +#ifndef WB_TEST_STUB_PROPERTY_OBJECT_H +#define WB_TEST_STUB_PROPERTY_OBJECT_H + +#include + +#include "Property.h" + +class PropertyObject { +public: + bool AddProperty(Property*) { return true; } + int32 Value(uint32, int32 defaultValue) const { return defaultValue; } + Property* FindProperty(uint32) const { return 0; } +}; + +#endif // WB_TEST_STUB_PROPERTY_OBJECT_H diff --git a/tests/stubs/RWLocker.h b/tests/stubs/RWLocker.h new file mode 100644 index 0000000..3759091 --- /dev/null +++ b/tests/stubs/RWLocker.h @@ -0,0 +1,10 @@ +#ifndef WB_TEST_STUB_RW_LOCKER_H +#define WB_TEST_STUB_RW_LOCKER_H + +class RWLocker { +public: + RWLocker(const char*) {} + virtual ~RWLocker() {} +}; + +#endif // WB_TEST_STUB_RW_LOCKER_H diff --git a/tests/stubs/Rect.h b/tests/stubs/Rect.h new file mode 100644 index 0000000..c4c94cb --- /dev/null +++ b/tests/stubs/Rect.h @@ -0,0 +1,46 @@ +#ifndef WB_TEST_STUB_RECT_H +#define WB_TEST_STUB_RECT_H + +#include "Point.h" + +class BRect { +public: + BRect() + : left(0) + , top(0) + , right(-1) + , bottom(-1) + { + } + + BRect(float l, float t, float r, float b) + : left(l) + , top(t) + , right(r) + , bottom(b) + { + } + + bool IsValid() const + { + return left <= right && top <= bottom; + } + + bool operator==(const BRect& other) const + { + return left == other.left && top == other.top && right == other.right + && bottom == other.bottom; + } + + bool operator!=(const BRect& other) const + { + return !(*this == other); + } + + float left; + float top; + float right; + float bottom; +}; + +#endif // WB_TEST_STUB_RECT_H diff --git a/tests/stubs/String.h b/tests/stubs/String.h new file mode 100644 index 0000000..f6aeab0 --- /dev/null +++ b/tests/stubs/String.h @@ -0,0 +1,43 @@ +#ifndef WB_TEST_STUB_STRING_H +#define WB_TEST_STUB_STRING_H + +#include + +class BString { +public: + BString() {} + BString(const char* string) + : fString(string != 0 ? string : "") + { + } + BString(const BString& other) + : fString(other.fString) + { + } + BString& operator=(const char* string) + { + fString = string != 0 ? string : ""; + return *this; + } + BString& operator=(const BString& other) + { + fString = other.fString; + return *this; + } + BString& operator<<(const char* string) + { + fString += string != 0 ? string : ""; + return *this; + } + bool operator==(const char* string) const + { + return fString == (string != 0 ? string : ""); + } + int Length() const { return (int)fString.size(); } + const char* String() const { return fString.c_str(); } + +private: + std::string fString; +}; + +#endif // WB_TEST_STUB_STRING_H diff --git a/tests/stubs/TestCompat.h b/tests/stubs/TestCompat.h new file mode 100644 index 0000000..af4eae5 --- /dev/null +++ b/tests/stubs/TestCompat.h @@ -0,0 +1,9 @@ +#ifndef WB_TEST_STUB_TEST_COMPAT_H +#define WB_TEST_STUB_TEST_COMPAT_H + +#ifndef WB_TEST_STUB_DEBUGGER_FUNCTION +#define WB_TEST_STUB_DEBUGGER_FUNCTION +extern "C" inline void debugger(const char*) {} +#endif + +#endif // WB_TEST_STUB_TEST_COMPAT_H diff --git a/tests/stubs/debugger.h b/tests/stubs/debugger.h new file mode 100644 index 0000000..10996a6 --- /dev/null +++ b/tests/stubs/debugger.h @@ -0,0 +1,9 @@ +#ifndef WB_TEST_STUB_DEBUGGER_H +#define WB_TEST_STUB_DEBUGGER_H + +#ifndef WB_TEST_STUB_DEBUGGER_FUNCTION +#define WB_TEST_STUB_DEBUGGER_FUNCTION +extern "C" inline void debugger(const char*) {} +#endif + +#endif // WB_TEST_STUB_DEBUGGER_H diff --git a/tests/stubs/image.h b/tests/stubs/image.h new file mode 100644 index 0000000..387eafc --- /dev/null +++ b/tests/stubs/image.h @@ -0,0 +1,8 @@ +#ifndef WB_TEST_STUB_IMAGE_H +#define WB_TEST_STUB_IMAGE_H + +#include + +typedef int32 thread_id; + +#endif // WB_TEST_STUB_IMAGE_H diff --git a/tests/support/BaseObjectStubs.cpp b/tests/support/BaseObjectStubs.cpp new file mode 100644 index 0000000..213ce04 --- /dev/null +++ b/tests/support/BaseObjectStubs.cpp @@ -0,0 +1,56 @@ +#include "BaseObject.h" +#include "CloneContext.h" + +#include + +// Shared minimal out-of-line implementations for focused tests that link +// BaseObject-derived model objects without pulling in the full UI/property +// stack. +Notifier::Notifier() + : fListeners(2) + , fSuspended(0) + , fPendingNotifications(false) +{ +} + +Notifier::~Notifier() {} +bool Notifier::AddListener(Listener*) { return false; } +bool Notifier::RemoveListener(Listener*) { return false; } +int32 Notifier::CountListeners() const { return 0; } +Listener* Notifier::ListenerAtFast(int32) const { return 0; } +void Notifier::Notify() {} +void Notifier::SuspendNotifications(bool) {} +void Notifier::NotifyListeners() {} + +BaseObject::BaseObject() + : Notifier() + , Referenceable() + , fName() +{ +} + +BaseObject::BaseObject(const BaseObject&) + : Notifier() + , Referenceable() + , fName() +{ +} + +BaseObject::BaseObject(const BMessage*) + : Notifier() + , Referenceable() + , fName() +{ +} + +BaseObject::~BaseObject() {} +BaseObject* BaseObject::Clone() const { CloneContext context; return Clone(context); } +status_t BaseObject::Unarchive(const BMessage*) { return B_OK; } +status_t BaseObject::Archive(BMessage*, bool) const { return B_OK; } +PropertyObject* BaseObject::MakePropertyObject() const { return 0; } +void BaseObject::AddProperties(PropertyObject*, uint32) const {} +bool BaseObject::SetToPropertyObject(const PropertyObject*, uint32) { return false; } +void BaseObject::SetName(const char*) {} +const char* BaseObject::Name() const { return DefaultName(); } +const BString& BaseObject::GivenName() const { return fName; } +bool BaseObject::GetIcon(const BBitmap*) const { return false; } diff --git a/tests/support/LayerDependencyStubs.cpp b/tests/support/LayerDependencyStubs.cpp new file mode 100644 index 0000000..14f5a49 --- /dev/null +++ b/tests/support/LayerDependencyStubs.cpp @@ -0,0 +1,66 @@ +#include "BoundedObject.h" +#include "Layer.h" +#include "LayerSnapshot.h" + +BoundedObject::BoundedObject() + : Object() + , fOpacity(255) + , fTransformedBounds() +{ +} + +BoundedObject::BoundedObject(const BoundedObject& other) + : Object(other) + , fOpacity(other.fOpacity) + , fTransformedBounds(other.fTransformedBounds) +{ +} + +BoundedObject::~BoundedObject() {} +void BoundedObject::AddProperties(PropertyObject*, uint32) const {} +bool BoundedObject::SetToPropertyObject(const PropertyObject*, uint32) { return false; } +void BoundedObject::TransformationChanged() {} +void BoundedObject::InitBounds() {} +void BoundedObject::UpdateBounds() {} +void BoundedObject::SetOpacity(uint8 opacity) { fOpacity = opacity; } +void BoundedObject::NotifyAndUpdate() {} + +ObjectSnapshot::ObjectSnapshot(const Object*) + : Transformable() + , fChangeCounter(0) + , fLayoutedState() + , fIsVisible(true) +{ +} + +ObjectSnapshot::~ObjectSnapshot() {} +bool ObjectSnapshot::Sync() { return false; } +void ObjectSnapshot::Layout(LayoutContext&, uint32) {} +void ObjectSnapshot::PrepareRendering(BRect) {} +void ObjectSnapshot::Render(RenderEngine&, RenderBuffer*, BRect) const {} +void ObjectSnapshot::RebuildAreaForDirtyArea(BRect&) const {} + +LayerSnapshot::LayerSnapshot(const ::Layer* layer) + : ObjectSnapshot(layer) + , fOriginal(layer) + , fObjects(8) + , fBounds() + , fBitmap(0) + , fGlobalAlpha(255) + , fBlendingMode(CompOpSrcOver) +{ +} + +LayerSnapshot::~LayerSnapshot() {} +const Object* LayerSnapshot::Original() const { return fOriginal; } +bool LayerSnapshot::Sync() { return false; } +void LayerSnapshot::Layout(LayoutContext&, uint32) {} +void LayerSnapshot::Render(RenderEngine&, RenderBuffer*, BRect) const {} +BRect LayerSnapshot::Bounds() const { return fBounds; } +BRect LayerSnapshot::Render(RenderEngine&, BRect area, RenderBuffer*, RenderBuffer*, + BRegion&, int32&) const { return area; } +ObjectSnapshot* LayerSnapshot::ObjectAt(int32) const { return 0; } +ObjectSnapshot* LayerSnapshot::ObjectAtFast(int32) const { return 0; } +int32 LayerSnapshot::CountObjects() const { return 0; } +void LayerSnapshot::_Sync() {} +void LayerSnapshot::_MakeEmpty() {} diff --git a/tests/support/Test.h b/tests/support/Test.h new file mode 100644 index 0000000..6d8fe6e --- /dev/null +++ b/tests/support/Test.h @@ -0,0 +1,27 @@ +#ifndef WB_TEST_H +#define WB_TEST_H + +#include +#include + +#define ASSERT_TRUE(expr) \ + do { \ + if (!(expr)) { \ + fprintf(stderr, "ASSERT_TRUE failed at %s:%d: %s\n", \ + __FILE__, __LINE__, #expr); \ + exit(1); \ + } \ + } while (false) + +#define ASSERT_EQ(expected, actual) \ + do { \ + auto _expected = (expected); \ + auto _actual = (actual); \ + if (!(_expected == _actual)) { \ + fprintf(stderr, "ASSERT_EQ failed at %s:%d: expected %lld, got %lld\n", \ + __FILE__, __LINE__, (long long)_expected, (long long)_actual); \ + exit(1); \ + } \ + } while (false) + +#endif // WB_TEST_H diff --git a/tests/support/TransformableStubs.cpp b/tests/support/TransformableStubs.cpp new file mode 100644 index 0000000..de19f06 --- /dev/null +++ b/tests/support/TransformableStubs.cpp @@ -0,0 +1,101 @@ +#include "Transformable.h" + +Transformable::Transformable() + : agg::trans_perspective() +{ +} + +Transformable::Transformable(const Transformable& other) + : agg::trans_perspective(other) +{ +} + +Transformable::~Transformable() {} + +void Transformable::StoreTo(double matrix[MatrixSize]) const +{ + store_to(matrix); +} + +void Transformable::LoadFrom(const double matrix[MatrixSize]) +{ + load_from(matrix); +} + +void Transformable::SetTransformable(const Transformable& other) +{ + *this = other; +} + +Transformable& Transformable::operator=(const Transformable& other) +{ + if (this != &other) { + reset(); + multiply(other); + } + return *this; +} + +Transformable& Transformable::Multiply(const Transformable& other) +{ + multiply(other); + return *this; +} + +Transformable& Transformable::PreMultiply(const Transformable& other) +{ + premultiply(other); + return *this; +} + +Transformable& Transformable::MultiplyInverse(const Transformable&) +{ + return *this; +} + +void Transformable::Reset() { reset(); } +void Transformable::Invert() { invert(); } +bool Transformable::IsValid() const { return true; } +bool Transformable::IsIdentity() const { return is_identity(); } +bool Transformable::IsTranslationOnly() const { return true; } +bool Transformable::IsNotDistorted() const { return true; } +bool Transformable::IsPerspective() const { return false; } +void Transformable::Transform(double*, double*) const {} +void Transformable::Transform(BPoint*) const {} +BPoint Transformable::Transform(const BPoint& point) const { return point; } +void Transformable::InverseTransform(double*, double*) const {} +void Transformable::InverseTransform(BPoint*) const {} +BPoint Transformable::InverseTransform(const BPoint& point) const { return point; } +BRect Transformable::TransformBounds(BRect bounds) const { return bounds; } +void Transformable::TranslateBy(BPoint) {} +void Transformable::RotateBy(BPoint, double) {} +void Transformable::ScaleBy(BPoint, double, double) {} +double Transformable::Scale() const { return 1.0; } +void Transformable::GetScale(double* scaleX, double* scaleY) const +{ + if (scaleX != 0) + *scaleX = 1.0; + if (scaleY != 0) + *scaleY = 1.0; +} + +bool Transformable::GetAffineParameters(double* translationX, + double* translationY, double* rotation, double* scaleX, double* scaleY, + double* skewX, double* skewY) const +{ + if (translationX != 0) + *translationX = 0.0; + if (translationY != 0) + *translationY = 0.0; + if (rotation != 0) + *rotation = 0.0; + if (scaleX != 0) + *scaleX = 1.0; + if (scaleY != 0) + *scaleY = 1.0; + if (skewX != 0) + *skewX = 0.0; + if (skewY != 0) + *skewY = 0.0; + return true; +}