Skip to content

Instantly share code, notes, and snippets.

@chriseidhof
Created February 21, 2020 08:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chriseidhof/36e6f8c95084a268c72c1c7297ac7a89 to your computer and use it in GitHub Desktop.
Save chriseidhof/36e6f8c95084a268c72c1c7297ac7a89 to your computer and use it in GitHub Desktop.
//
// ContentView.swift
// QuickCheckTests
//
// Created by Chris Eidhof on 20.02.20.
// Copyright © 2020 objc.io. All rights reserved.
//
import SwiftUI
final class Generator<A>: ObservableObject {
@Published var value: A
@Published var count = 0
private let generate: () -> A
init(_ generate: @escaping () -> A) {
self.generate = generate
value = generate()
}
func next() {
value = generate()
count += 1
}
}
extension View {
private static func helper(condition: Bool, message: String?, line: UInt) -> some View {
assert(condition, message ?? "Assert line \(line)", line: line)
return Color.clear
}
func asserting(_ condition: @escaping (CGSize) -> Bool, line: UInt = #line, _ message: @escaping (CGSize) -> String?) -> some View {
overlay(GeometryReader { proxy in
Self.helper(condition: condition(proxy.size), message: message(proxy.size), line: line)
})
}
func assertingEquals<A: Equatable>(_ lhs: @escaping (CGSize) -> A, _ rhs: A, line: UInt = #line, _ message: String? = nil) -> some View {
overlay(GeometryReader { proxy in
Self.helper(condition: lhs(proxy.size) == rhs, message: message ?? "Expected \(lhs(proxy.size)) == \(rhs) (proxy size was \(proxy.size))", line: line)
})
}
}
enum ProposedSize {
case fixed(CGSize)
case random
}
struct QuickCheck<A: Equatable, V: View>: View {
@ObservedObject var generator: Generator<A>
@ObservedObject var sizeGenerator: Generator<CGSize> = Generator {
CGSize(width: nonZeroCGFloat(), height: nonZeroCGFloat())
}
var view: (A) -> V
private let check: (CGSize, CGSize, A) -> Bool
private var line: UInt
private var proposedSize: ProposedSize
var description: String
init(_ description: String, _ generate: @escaping () -> A, check: @escaping (CGSize, CGSize, A) -> Bool, line: UInt = #line, proposedSize: ProposedSize = .fixed(CGSize(width: 200, height: 200)), @ViewBuilder view: @escaping (A) -> V) {
self.description = description
self.view = view
self.check = check
self.line = line
self.proposedSize = proposedSize
self.generator = Generator(generate)
}
var frame: CGSize {
switch proposedSize {
case .fixed(let x): return x
case .random: return self.sizeGenerator.value
}
}
var body: some View {
VStack {
view(generator.value)
.asserting({ self.check($0, self.frame, self.generator.value) }, line: line, { size in "Test \(self.generator.count) failed. Size: \(size). Value: \(self.generator.value). Proposed: \(self.frame). (\(self.description)" })
.frame(width: frame.width, height: frame.height)
.id(generator.count)
.onAppear {
if self.generator.count < 10_000 {
DispatchQueue.main.async {
self.generator.next()
self.sizeGenerator.next()
}
} else {
print("Passed \(self.generator.count) tests")
}
}
Text("\(generator.count)")
}
}
}
let nonZeroCGFloat = { CGFloat.random(in: 0..<10_000).rounded() }
struct MinMax: Equatable {
var min: CGFloat
var max: CGFloat
}
let minMax: () -> MinMax = {
let min = CGFloat.random(in: 0..<5_000).rounded()
let max = CGFloat.random(in: min..<10_000).rounded()
return MinMax(min: min, max: max)
}
struct MinMaxIdeal: Equatable {
var min: CGFloat
var max: CGFloat
var ideal: CGFloat
static func random() -> MinMaxIdeal {
let min = CGFloat.random(in: 0..<5_000).rounded()
let max = CGFloat.random(in: min..<10_000).rounded()
let ideal = CGFloat.random(in: min...max).rounded()
return MinMaxIdeal(min: min, max: max, ideal: ideal)
}
}
extension CGFloat {
func roughlyEquals(_ other: CGFloat, epsilon: CGFloat = 1) -> Bool {
return abs(self - other) < epsilon
}
}
struct ContentView: View {
var body: some View {
VStack {
QuickCheck("A frame is always the max of it's child width and minWidth", nonZeroCGFloat, check: { size, _, value in
size.width.roughlyEquals(max(value, 100))
}) { value in
Rectangle()
.frame(width: 100, height: 100)
.frame(minWidth: value)
}
QuickCheck("A frame is always the min of (the max of its child width and proposedWidth) and maxWidth", nonZeroCGFloat, check: { size, proposed, value in
size.width.roughlyEquals(min(value, max(100, proposed.width)))
}, proposedSize: .random) { value in
Rectangle()
.frame(width: 100, height: 100)
.frame(maxWidth: value)
}
QuickCheck("A frame with both a min and max width becomes max(minWidth, min(maxWidth, proposedSize.width))", minMax, check: { size, proposedSize, value in
size.width.roughlyEquals(max(value.min, min(value.max, proposedSize.width)))
}, proposedSize: .random) { (value: MinMax) in
Rectangle()
.fill(Color.green)
.frame(width: 100, height: 50)
.frame(minWidth: value.min, maxWidth: value.max)
}
QuickCheck("A frame with an ideal width becomes it ideal width", nonZeroCGFloat, check: { size, proposedSize, value in
size.width.roughlyEquals(value)
}, proposedSize: .random) { (value: CGFloat) in
Rectangle()
.fill(Color.green)
.frame(width: 100, height: 50)
.frame(idealWidth: value)
.fixedSize()
}
QuickCheck("A frame with a min, max and ideal width becomes it ideal width", MinMaxIdeal.random, check: { (size: CGSize, proposedSize: CGSize, value: MinMaxIdeal) in
size.width.roughlyEquals(value.ideal)
}, proposedSize: .random) { (value: MinMaxIdeal) in
Rectangle()
.fill(Color.green)
.frame(width: 100, height: 50)
.frame(minWidth: value.min, idealWidth: value.ideal, maxWidth: value.max)
.fixedSize()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment