Skip to content

Commit

Permalink
Add CoreGraphics.Drawing UT and a bunch of image rendering tests.
Browse files Browse the repository at this point in the history
This change set introduces DRAW_TEST and DRAW_TEST_F, as well as a handful of
image drawing test fixtures. Commands rendered to a test's context as part of
its test body will be rasterized and saved in a file named
TestImage.$TESTCASE.$TESTNAME.png

The test driver's ideal interface is multi-modal; it needs to be able to
generate a corpus of reference images on disk, and it needs to be able to diff
them. For this, it needs a custom EntryPoint (included), and the ability to
parse command-line arguments.

The ideal default mode will be one that loads reference images from disk and
fails tests if a number of pixels differ. That work is not yet complete.

Hopefully, the draw tests here will be able to be plugged into another module
that can perhaps render them to screen, or display them as part of a UI-driven
flow for comparison and demoing purposes.

Refs #1271.
  • Loading branch information
DHowett committed Nov 15, 2016
1 parent f3c082e commit ba6c5ca
Show file tree
Hide file tree
Showing 12 changed files with 1,305 additions and 10 deletions.

Large diffs are not rendered by default.

31 changes: 21 additions & 10 deletions build/build.sln
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AddressBook", "AddressBook"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AddressBook.UnitTests", "Tests\UnitTests\AddressBook\AddressBook.UnitTests.vcxproj", "{62E53898-65C2-4401-BF58-FBFB728E1B27}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CoreGraphics.Drawing.UnitTests", "Tests\UnitTests\CoreGraphics.Drawing\CoreGraphics.Drawing.UnitTests.vcxproj", "{DE51CDE9-F326-49B6-8C5B-35D5B091878C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM = Debug|ARM
Expand Down Expand Up @@ -1905,6 +1907,14 @@ Global
{926FBB0B-874F-4F88-84EB-4352D41CF810}.Release|ARM.Build.0 = Release|ARM
{926FBB0B-874F-4F88-84EB-4352D41CF810}.Release|x86.ActiveCfg = Release|Win32
{926FBB0B-874F-4F88-84EB-4352D41CF810}.Release|x86.Build.0 = Release|Win32
{1884D8F8-2C05-4334-A778-7D3C5A6736E8}.Debug|ARM.ActiveCfg = Debug|ARM
{1884D8F8-2C05-4334-A778-7D3C5A6736E8}.Debug|ARM.Build.0 = Debug|ARM
{1884D8F8-2C05-4334-A778-7D3C5A6736E8}.Debug|x86.ActiveCfg = Debug|Win32
{1884D8F8-2C05-4334-A778-7D3C5A6736E8}.Debug|x86.Build.0 = Debug|Win32
{1884D8F8-2C05-4334-A778-7D3C5A6736E8}.Release|ARM.ActiveCfg = Release|ARM
{1884D8F8-2C05-4334-A778-7D3C5A6736E8}.Release|ARM.Build.0 = Release|ARM
{1884D8F8-2C05-4334-A778-7D3C5A6736E8}.Release|x86.ActiveCfg = Release|Win32
{1884D8F8-2C05-4334-A778-7D3C5A6736E8}.Release|x86.Build.0 = Release|Win32
{ACD0E309-4AE5-440D-AB09-006EEA6AFB60}.Debug|ARM.ActiveCfg = Debug|ARM
{ACD0E309-4AE5-440D-AB09-006EEA6AFB60}.Debug|ARM.Build.0 = Debug|ARM
{ACD0E309-4AE5-440D-AB09-006EEA6AFB60}.Debug|x86.ActiveCfg = Debug|Win32
Expand All @@ -1929,14 +1939,6 @@ Global
{DA6C01EA-A22E-4807-BACE-63C5C6ABAE77}.Release|ARM.Build.0 = Release|ARM
{DA6C01EA-A22E-4807-BACE-63C5C6ABAE77}.Release|x86.ActiveCfg = Release|Win32
{DA6C01EA-A22E-4807-BACE-63C5C6ABAE77}.Release|x86.Build.0 = Release|Win32
{1884D8F8-2C05-4334-A778-7D3C5A6736E8}.Debug|ARM.ActiveCfg = Debug|ARM
{1884D8F8-2C05-4334-A778-7D3C5A6736E8}.Debug|ARM.Build.0 = Debug|ARM
{1884D8F8-2C05-4334-A778-7D3C5A6736E8}.Debug|x86.ActiveCfg = Debug|Win32
{1884D8F8-2C05-4334-A778-7D3C5A6736E8}.Debug|x86.Build.0 = Debug|Win32
{1884D8F8-2C05-4334-A778-7D3C5A6736E8}.Release|ARM.ActiveCfg = Release|ARM
{1884D8F8-2C05-4334-A778-7D3C5A6736E8}.Release|ARM.Build.0 = Release|ARM
{1884D8F8-2C05-4334-A778-7D3C5A6736E8}.Release|x86.ActiveCfg = Release|Win32
{1884D8F8-2C05-4334-A778-7D3C5A6736E8}.Release|x86.Build.0 = Release|Win32
{0B1B276A-40B2-4E7A-BA77-7B8BD7F6D3D3}.Debug|ARM.ActiveCfg = Debug|ARM
{0B1B276A-40B2-4E7A-BA77-7B8BD7F6D3D3}.Debug|ARM.Build.0 = Debug|ARM
{0B1B276A-40B2-4E7A-BA77-7B8BD7F6D3D3}.Debug|x86.ActiveCfg = Debug|Win32
Expand All @@ -1953,6 +1955,14 @@ Global
{62E53898-65C2-4401-BF58-FBFB728E1B27}.Release|ARM.Build.0 = Release|ARM
{62E53898-65C2-4401-BF58-FBFB728E1B27}.Release|x86.ActiveCfg = Release|Win32
{62E53898-65C2-4401-BF58-FBFB728E1B27}.Release|x86.Build.0 = Release|Win32
{DE51CDE9-F326-49B6-8C5B-35D5B091878C}.Debug|ARM.ActiveCfg = Debug|ARM
{DE51CDE9-F326-49B6-8C5B-35D5B091878C}.Debug|ARM.Build.0 = Debug|ARM
{DE51CDE9-F326-49B6-8C5B-35D5B091878C}.Debug|x86.ActiveCfg = Debug|Win32
{DE51CDE9-F326-49B6-8C5B-35D5B091878C}.Debug|x86.Build.0 = Debug|Win32
{DE51CDE9-F326-49B6-8C5B-35D5B091878C}.Release|ARM.ActiveCfg = Release|ARM
{DE51CDE9-F326-49B6-8C5B-35D5B091878C}.Release|ARM.Build.0 = Release|ARM
{DE51CDE9-F326-49B6-8C5B-35D5B091878C}.Release|x86.ActiveCfg = Release|Win32
{DE51CDE9-F326-49B6-8C5B-35D5B091878C}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -2242,15 +2252,16 @@ Global
{5DAC2E58-D2CE-4590-A3B5-9DAB3076DD33} = {A44DA03C-DDA0-495A-9168-30CA6BF87CA5}
{85AE5EC2-24A1-4A23-B176-D89B79E85804} = {2F82C9C9-AEE7-4E2B-80F0-72780A265A8D}
{926FBB0B-874F-4F88-84EB-4352D41CF810} = {5DAC2E58-D2CE-4590-A3B5-9DAB3076DD33}
{0B0B4AA9-A0C5-4A63-8EE0-D0375B68D913} = {91917994-EA93-4A8C-A149-9A32EB2DAF2C}
{1884D8F8-2C05-4334-A778-7D3C5A6736E8} = {0B0B4AA9-A0C5-4A63-8EE0-D0375B68D913}
{FA31D437-C064-4F51-B9DE-44EEB0E7EA76} = {88413F6C-C27A-4B48-9AE5-D36161920F6D}
{ACD0E309-4AE5-440D-AB09-006EEA6AFB60} = {FA31D437-C064-4F51-B9DE-44EEB0E7EA76}
{4FDF4507-8C1E-4617-9278-11D4BFE8F3A5} = {5995B891-E02B-4FA4-8D14-82997198372F}
{0A79AFE5-2685-433C-BC78-A4AD09CD7BF8} = {88413F6C-C27A-4B48-9AE5-D36161920F6D}
{DA6C01EA-A22E-4807-BACE-63C5C6ABAE77} = {0A79AFE5-2685-433C-BC78-A4AD09CD7BF8}
{1884D8F8-2C05-4334-A778-7D3C5A6736E8} = {0B0B4AA9-A0C5-4A63-8EE0-D0375B68D913}
{0B0B4AA9-A0C5-4A63-8EE0-D0375B68D913} = {91917994-EA93-4A8C-A149-9A32EB2DAF2C}
{0B1B276A-40B2-4E7A-BA77-7B8BD7F6D3D3} = {0DBB776F-4BD0-48A6-9CA7-E8EB08ECC269}
{69D1F829-1843-4395-BCE9-69C4CEB59004} = {88413F6C-C27A-4B48-9AE5-D36161920F6D}
{62E53898-65C2-4401-BF58-FBFB728E1B27} = {69D1F829-1843-4395-BCE9-69C4CEB59004}
{DE51CDE9-F326-49B6-8C5B-35D5B091878C} = {0A79AFE5-2685-433C-BC78-A4AD09CD7BF8}
EndGlobalSection
EndGlobal
149 changes: 149 additions & 0 deletions tests/UnitTests/CoreGraphics.drawing/CGContextDrawingTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//******************************************************************************
//
// Copyright (c) Microsoft. All rights reserved.
//
// This code is licensed under the MIT License (MIT).
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
//******************************************************************************

#include "DrawingTest.h"

DRAW_TEST_F(CGContext, RedBox, UIKitMimicTest) {
CGContextRef context = GetDrawingContext();
CGRect bounds = GetDrawingBounds();

CGContextSetRGBFillColor(context, 1.0, 0.0, 0.0, 1.0);
CGContextFillRect(context, CGRectInset(bounds, 10, 10));
}

DRAW_TEST_F(CGContext, FillThenStrokeIsSameAsDrawFillStroke, WhiteBackgroundTest) {
CGContextRef context = GetDrawingContext();
CGRect bounds = GetDrawingBounds();

// Black with a faint red outline will allow us to see through the red.
CGContextSetRGBFillColor(context, 0.0, 0.0, 0.0, 1.0);
CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 0.33);
CGContextSetLineWidth(context, 5.f);

CGPoint leftCenter{ bounds.size.width / 4.f, bounds.size.height / 2.f };
CGPoint rightCenter{ 3.f * bounds.size.width / 4.f, bounds.size.height / 2.f };

// Left circle, fill then stroke.
CGContextBeginPath(context);
CGContextAddEllipseInRect(context, _CGRectCenteredOnPoint({ 150, 150 }, leftCenter));
CGContextFillPath(context);
CGContextBeginPath(context);
CGContextAddEllipseInRect(context, _CGRectCenteredOnPoint({ 150, 150 }, leftCenter));
CGContextStrokePath(context);

// Right circle, all at once
CGContextBeginPath(context);
CGContextAddEllipseInRect(context, _CGRectCenteredOnPoint({ 150, 150 }, rightCenter));
CGContextDrawPath(context, kCGPathFillStroke);
}

static void _drawThreeCirclesInContext(CGContextRef context, CGRect bounds) {
CGPoint center = _CGRectGetCenter(bounds);
CGRect centerEllipseRect = _CGRectCenteredOnPoint({ 150, 150 }, center);
CGFloat translations[]{ -60.f, 0.f, +60.f };

for (float xSlide : translations) {
CGRect translatedRect = CGRectApplyAffineTransform(centerEllipseRect, CGAffineTransformMakeTranslation(xSlide, 0));
CGContextFillEllipseInRect(context, translatedRect);
CGContextStrokeEllipseInRect(context, translatedRect);
}
}

DRAW_TEST_F(CGContext, OverlappingCirclesColorAlpha, WhiteBackgroundTest) {
CGContextRef context = GetDrawingContext();
CGRect bounds = GetDrawingBounds();

CGContextSetRGBFillColor(context, 1.0, 0.0, 0.0, 0.5);
CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
CGContextSetLineWidth(context, 5);

_drawThreeCirclesInContext(context, bounds);
}

DRAW_TEST_F(CGContext, OverlappingCirclesGlobalAlpha, WhiteBackgroundTest) {
CGContextRef context = GetDrawingContext();
CGRect bounds = GetDrawingBounds();

CGContextSetRGBFillColor(context, 1.0, 0.0, 0.0, 1.0);
CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
CGContextSetLineWidth(context, 5);

CGContextSetAlpha(context, 0.5);

_drawThreeCirclesInContext(context, bounds);
}

DRAW_TEST_F(CGContext, OverlappingCirclesGlobalAlphaStackedWithColorAlpha, WhiteBackgroundTest) {
CGContextRef context = GetDrawingContext();
CGRect bounds = GetDrawingBounds();

CGContextSetRGBFillColor(context, 1.0, 0.0, 0.0, 0.5);
CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
CGContextSetLineWidth(context, 5);

CGContextSetAlpha(context, 0.75);

_drawThreeCirclesInContext(context, bounds);
}

DISABLED_DRAW_TEST_F(CGContext, OverlappingCirclesTransparencyLayerAlpha, WhiteBackgroundTest) {
CGContextRef context = GetDrawingContext();
CGRect bounds = GetDrawingBounds();

CGContextSetRGBFillColor(context, 1.0, 0.0, 0.0, 1.0);
CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
CGContextSetLineWidth(context, 5);

CGContextSetAlpha(context, 0.5);

CGContextBeginTransparencyLayer(context, nullptr);

_drawThreeCirclesInContext(context, bounds);

CGContextEndTransparencyLayer(context);
}

// This test proves that the path is stored fully transformed;
// changing the CTM before stroking it does not cause it to scale!
// However, the stroke width _is_ scaled (!)
DRAW_TEST_F(CGContext, ChangeCTMAfterCreatingPath, WhiteBackgroundTest) {
CGContextRef context = GetDrawingContext();
CGRect bounds = GetDrawingBounds();

CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
CGContextSetLineWidth(context, 1);

CGContextBeginPath(context);
CGContextMoveToPoint(context, 5, 5.5);
CGContextAddLineToPoint(context, (bounds.size.width - 5) / 3, 5.5);
CGContextStrokePath(context);

CGContextSaveGState(context);
CGContextBeginPath(context);
CGContextMoveToPoint(context, 5, 10.5);
CGContextAddLineToPoint(context, (bounds.size.width - 5) / 3, 10.5);
CGContextScaleCTM(context, 2.0, 2.0);
CGContextStrokePath(context);
CGContextRestoreGState(context);

CGContextSaveGState(context);
CGContextBeginPath(context);
CGContextMoveToPoint(context, 5, 15.5);
CGContextAddLineToPoint(context, (bounds.size.width - 5) / 3, 15.5);
CGContextScaleCTM(context, 3.0, 3.0);
CGContextStrokePath(context);
CGContextRestoreGState(context);
}
165 changes: 165 additions & 0 deletions tests/UnitTests/CoreGraphics.drawing/DrawingTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//******************************************************************************
//
// Copyright (c) Microsoft. All rights reserved.
//
// This code is licensed under the MIT License (MIT).
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
//******************************************************************************

#include "DrawingTest.h"

#include <CoreFoundation/CoreFoundation.h>
#include <ImageIO/ImageIO.h>

#include <Starboard/SmartTypes.h>
#include <memory>

static const CGSize g_defaultCanvasSize{ 512.f, 256.f };

woc::unique_cf<CGColorSpaceRef> testing::DrawTest::s_deviceColorSpace;

void testing::DrawTest::SetUpTestCase() {
s_deviceColorSpace.reset(CGColorSpaceCreateDeviceRGB());
}

void testing::DrawTest::TearDownTestCase() {
s_deviceColorSpace.release();
}

CGSize testing::DrawTest::CanvasSize() {
return g_defaultCanvasSize;
}

void testing::DrawTest::SetUp() {
CGSize size = CanvasSize();

_context.reset(CGBitmapContextCreate(
nullptr, size.width, size.height, 8, size.width * 4, s_deviceColorSpace.get(), kCGImageAlphaPremultipliedFirst));
ASSERT_NE(nullptr, _context);

_bounds = { CGPointZero, size };

SetUpContext();
}

CFStringRef testing::DrawTest::CreateAdditionalTestDescription() {
return nullptr;
}

CFStringRef testing::DrawTest::CreateOutputFilename() {
const ::testing::TestInfo* const test_info = ::testing::UnitTest::GetInstance()->current_test_info();
woc::unique_cf<CFStringRef> additionalDesc{ CreateAdditionalTestDescription() };
woc::unique_cf<CFStringRef> filename{ CFStringCreateWithFormat(nullptr,
nullptr,
CFSTR("TestImage.%s.%s%s%@.png"),
test_info->test_case_name(),
test_info->name(),
(additionalDesc ? "." : ""),
(additionalDesc ? additionalDesc.get() : CFSTR(""))) };
return filename.release();
}

void testing::DrawTest::TearDown() {
CGContextRef context = GetDrawingContext();

woc::unique_cf<CGImageRef> image{ CGBitmapContextCreateImage(context) };
ASSERT_NE(nullptr, image);

woc::unique_cf<CFMutableDataRef> imageData{ CFDataCreateMutable(nullptr, 1048576) };
woc::unique_cf<CGImageDestinationRef> imageDest{ CGImageDestinationCreateWithData(imageData.get(), CFSTR("public.png"), 1, nullptr) };
CGImageDestinationAddImage(imageDest.get(), image.get(), nullptr);
CGImageDestinationFinalize(imageDest.get());

woc::unique_cf<CFStringRef> originalFilename{ CreateOutputFilename() };

woc::unique_cf<CFMutableStringRef> filename{ CFStringCreateMutableCopy(nullptr, 0, originalFilename.get()) };

CFStringFindAndReplace(filename.get(), CFSTR("DISABLED_"), CFSTR(""), CFRange{ 0, CFStringGetLength(filename.get()) }, 0);
CFStringFindAndReplace(filename.get(), CFSTR("/"), CFSTR("_"), CFRange{ 0, CFStringGetLength(filename.get()) }, 0);

// This is only populated if CFStringGetCStringPtr fails.
std::unique_ptr<char[]> owningFilenamePtr;

char* rawFilename = const_cast<char*>(CFStringGetCStringPtr(filename.get(), kCFStringEncodingUTF8));
size_t len = 0;

if (!rawFilename) {
CFRange filenameRange{ 0, CFStringGetLength(filename.get()) };
CFIndex requiredBufferLength = 0;
CFStringGetBytes(filename.get(), filenameRange, kCFStringEncodingUTF8, 0, FALSE, nullptr, 0, &requiredBufferLength);
owningFilenamePtr.reset(new char[requiredBufferLength]);
rawFilename = owningFilenamePtr.get();
CFStringGetBytes(filename.get(),
filenameRange,
kCFStringEncodingUTF8,
0,
FALSE,
(UInt8*)rawFilename,
requiredBufferLength,
&requiredBufferLength);
len = requiredBufferLength;
} else {
len = strlen(rawFilename);
}
woc::unique_cf<CFURLRef> url{ CFURLCreateFromFileSystemRepresentation(nullptr, (UInt8*)rawFilename, strlen(rawFilename), FALSE) };
ASSERT_TRUE(CFURLWriteDataAndPropertiesToResource(url.get(), imageData.get(), nullptr, nullptr));
}

void testing::DrawTest::SetUpContext() {
// The default context is fine as-is.
}

void testing::DrawTest::TestBody() {
// Nothing.
}

CGContextRef testing::DrawTest::GetDrawingContext() {
return _context.get();
}

void testing::DrawTest::SetDrawingBounds(CGRect bounds) {
_bounds = bounds;
}

CGRect testing::DrawTest::GetDrawingBounds() {
return _bounds;
}

void WhiteBackgroundTest::SetUpContext() {
CGContextRef context = GetDrawingContext();
CGRect bounds = GetDrawingBounds();

CGContextSaveGState(context);
CGContextSetRGBFillColor(context, 1.0, 1.0, 1.0, 1.0);
CGContextFillRect(context, bounds);
CGContextRestoreGState(context);

CGContextSetRGBStrokeColor(context, 0.0, 0.0, 0.0, 1.0);
}

CGSize UIKitMimicTest::CanvasSize() {
CGSize parent = WhiteBackgroundTest::CanvasSize();
return { parent.width * 2., parent.height * 2. };
}

void UIKitMimicTest::SetUpContext() {
WhiteBackgroundTest::SetUpContext();

CGContextRef context = GetDrawingContext();
CGRect bounds = GetDrawingBounds();

CGContextScaleCTM(context, 1.0, -1.0);
CGContextTranslateCTM(context, 0, -bounds.size.height);
CGContextScaleCTM(context, 2.0, 2.0);
bounds = CGRectApplyAffineTransform(bounds, CGAffineTransformMakeScale(.5, .5));

SetDrawingBounds(bounds);
}
Loading

0 comments on commit ba6c5ca

Please sign in to comment.