Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

159 unit test astronomical calculations #162

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions KosherCocoa.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,17 @@
4618EE1C1BF4F9F900C33159 /* NSDateComponents+AllComponents.m in Sources */ = {isa = PBXBuildFile; fileRef = 466A3ACE17CAF3B900056F16 /* NSDateComponents+AllComponents.m */; };
469066561F4A7090009E5D46 /* KosherCocoa.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 469066551F4A7090009E5D46 /* KosherCocoa.podspec */; };
46DB49691D9839F200F3A576 /* KosherCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4618ECE51BF449EF00C33159 /* KosherCocoa.framework */; };
5D435CEA2A3C34CB00F66AE0 /* KCAstronomicalCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D435CE92A3C34CB00F66AE0 /* KCAstronomicalCalculatorTests.swift */; };
5D435CEA2A3C34CB00F66AE0 /* KCNOAACalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D435CE92A3C34CB00F66AE0 /* KCNOAACalculatorTests.swift */; };
5D435CEC2A3C352500F66AE0 /* KCNOAACalculator.h in Headers */ = {isa = PBXBuildFile; fileRef = 5D435CEB2A3C352500F66AE0 /* KCNOAACalculator.h */; settings = {ATTRIBUTES = (Public, ); }; };
5D435CEF2A3C35B000F66AE0 /* KCNOAACalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D435CEE2A3C35B000F66AE0 /* KCNOAACalculator.m */; };
5D435CF22A3C369900F66AE0 /* KCAstronomicalCalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D435CF12A3C369900F66AE0 /* KCAstronomicalCalculator.m */; };
5D435CF42A3C389C00F66AE0 /* Screenshot 2023-06-16 at 2.27.26 AM.png in Resources */ = {isa = PBXBuildFile; fileRef = 5D435CF32A3C389B00F66AE0 /* Screenshot 2023-06-16 at 2.27.26 AM.png */; };
F52587431ECD019A009E623C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F52587411ECD019A009E623C /* Localizable.strings */; };
F5C6076F2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C6076E2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift */; };
F5C607722B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv in Resources */ = {isa = PBXBuildFile; fileRef = F5C607712B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv */; };
F5C607772B3F5A4E00A1E94C /* DynamicTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C607762B3F5A4E00A1E94C /* DynamicTestCase.m */; };
F5C607792B3F5BE600A1E94C /* TestLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C607782B3F5BE600A1E94C /* TestLoader.swift */; };
F5C607B72B4226BA00A1E94C /* DynamicNOAACalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C607B62B4226BA00A1E94C /* DynamicNOAACalculatorTests.swift */; };
F5F902FA1FC53B17003FE90D /* KosherCocoaMetadataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46DB495A1D9837D100F3A576 /* KosherCocoaMetadataTests.swift */; };
F5F902FC1FC53C01003FE90D /* KCJewishCalendarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5F902FB1FC53C01003FE90D /* KCJewishCalendarTests.swift */; };
/* End PBXBuildFile section */
Expand Down Expand Up @@ -174,12 +179,19 @@
46DB495A1D9837D100F3A576 /* KosherCocoaMetadataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KosherCocoaMetadataTests.swift; sourceTree = "<group>"; };
46DB495C1D9837D100F3A576 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
46DB49641D9839F100F3A576 /* KosherCocoaTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KosherCocoaTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
5D435CE92A3C34CB00F66AE0 /* KCAstronomicalCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KCAstronomicalCalculatorTests.swift; sourceTree = "<group>"; };
5D435CE92A3C34CB00F66AE0 /* KCNOAACalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KCNOAACalculatorTests.swift; sourceTree = "<group>"; };
5D435CEB2A3C352500F66AE0 /* KCNOAACalculator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KCNOAACalculator.h; sourceTree = "<group>"; };
5D435CEE2A3C35B000F66AE0 /* KCNOAACalculator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KCNOAACalculator.m; sourceTree = "<group>"; };
5D435CF12A3C369900F66AE0 /* KCAstronomicalCalculator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KCAstronomicalCalculator.m; sourceTree = "<group>"; };
5D435CF32A3C389B00F66AE0 /* Screenshot 2023-06-16 at 2.27.26 AM.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Screenshot 2023-06-16 at 2.27.26 AM.png"; sourceTree = "<group>"; };
F52587421ECD019A009E623C /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = Localizations/en.lproj/Localizable.strings; sourceTree = "<group>"; };
F5C6076E2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KCAstronomicalCalculatorTests.swift; sourceTree = "<group>"; };
F5C607712B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SunriseSunsetLakewoodNOAA.csv; sourceTree = "<group>"; };
F5C607752B3F5A4D00A1E94C /* KosherCocoaTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "KosherCocoaTests-Bridging-Header.h"; sourceTree = "<group>"; };
F5C607762B3F5A4E00A1E94C /* DynamicTestCase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DynamicTestCase.m; sourceTree = "<group>"; };
F5C607782B3F5BE600A1E94C /* TestLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestLoader.swift; sourceTree = "<group>"; };
F5C607B52B42158500A1E94C /* DynamicTestCase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DynamicTestCase.h; sourceTree = "<group>"; };
F5C607B62B4226BA00A1E94C /* DynamicNOAACalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicNOAACalculatorTests.swift; sourceTree = "<group>"; };
F5F902FB1FC53C01003FE90D /* KCJewishCalendarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KCJewishCalendarTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -454,11 +466,18 @@
46DB49591D9837D100F3A576 /* KosherCocoaTests */ = {
isa = PBXGroup;
children = (
5D435CE92A3C34CB00F66AE0 /* KCAstronomicalCalculatorTests.swift */,
F5C607712B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv */,
5D435CE92A3C34CB00F66AE0 /* KCNOAACalculatorTests.swift */,
F5C607782B3F5BE600A1E94C /* TestLoader.swift */,
F5C607762B3F5A4E00A1E94C /* DynamicTestCase.m */,
F5C607B52B42158500A1E94C /* DynamicTestCase.h */,
F5C607B62B4226BA00A1E94C /* DynamicNOAACalculatorTests.swift */,
F5C6076E2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift */,
46DB495A1D9837D100F3A576 /* KosherCocoaMetadataTests.swift */,
F5F902FB1FC53C01003FE90D /* KCJewishCalendarTests.swift */,
5D435CF32A3C389B00F66AE0 /* Screenshot 2023-06-16 at 2.27.26 AM.png */,
46DB495C1D9837D100F3A576 /* Info.plist */,
F5C607752B3F5A4D00A1E94C /* KosherCocoaTests-Bridging-Header.h */,
);
path = KosherCocoaTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -571,7 +590,7 @@
};
46DB49631D9839F100F3A576 = {
CreatedOnToolsVersion = 8.0;
LastSwiftMigration = "";
LastSwiftMigration = 1510;
ProvisioningStyle = Automatic;
};
};
Expand Down Expand Up @@ -611,6 +630,7 @@
buildActionMask = 2147483647;
files = (
5D435CF42A3C389C00F66AE0 /* Screenshot 2023-06-16 at 2.27.26 AM.png in Resources */,
F5C607722B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -656,9 +676,13 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5D435CEA2A3C34CB00F66AE0 /* KCAstronomicalCalculatorTests.swift in Sources */,
5D435CEA2A3C34CB00F66AE0 /* KCNOAACalculatorTests.swift in Sources */,
F5C6076F2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift in Sources */,
F5F902FA1FC53B17003FE90D /* KosherCocoaMetadataTests.swift in Sources */,
F5C607B72B4226BA00A1E94C /* DynamicNOAACalculatorTests.swift in Sources */,
F5F902FC1FC53C01003FE90D /* KCJewishCalendarTests.swift in Sources */,
F5C607792B3F5BE600A1E94C /* TestLoader.swift in Sources */,
F5C607772B3F5A4E00A1E94C /* DynamicTestCase.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -897,6 +921,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OBJC_BRIDGING_HEADER = "KosherCocoaTests/KosherCocoaTests-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
Expand Down Expand Up @@ -928,6 +953,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.mosheberman.KosherCocoaTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator";
SWIFT_OBJC_BRIDGING_HEADER = "KosherCocoaTests/KosherCocoaTests-Bridging-Header.h";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,3,4";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
version = "1.3">
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
Expand All @@ -11,6 +11,12 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:KosherCocoaTests/KosherCocoaTests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO">
Expand Down
2 changes: 1 addition & 1 deletion KosherCocoa/Library/Core/Solar/KCNOAACalculator.m
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ - (nullable NSDateComponents *)yearMonthAndDayFromDate:(nonnull NSDate *)date
//

NSCalendar *gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];

gregorianCalendar.timeZone = self.geoLocation.timeZone;
//
// Set up the date components
//
Expand Down
153 changes: 153 additions & 0 deletions KosherCocoaTests/DynamicNOAACalculatorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//
// DynamicNOAACalculatorTests.swift
// KosherCocoaTests
//
// Created by Moshe Berman on 12/31/23.
// Copyright © 2023 Moshe Berman. All rights reserved.
//

import XCTest
import Foundation
import KosherCocoa

class DynamicNOAACalculatorTestCase : DynamicTestCase {
static var timeZone: TimeZone = {
TimeZone(identifier: "America/New_York")!
}()
static var lakewood: GeoLocation = {
GeoLocation(
latitude: 40.096,
andLongitude: -74.222,
elevation: 25.58,
andTimeZone: timeZone
)
}()

static var calendar: AstronomicalCalendar = {
let calculator = NOAACalculator(geoLocation: lakewood)
let astroCalendar = AstronomicalCalendar(location: lakewood)
astroCalendar.astronomicalCalculator = calculator
return astroCalendar
}()


/// A reusable date formatter.
private static var dateFormatter: DateFormatter = {

let formatter = DateFormatter()

formatter.setLocalizedDateFormatFromTemplate("MMM dd, yyyy")
formatter.timeZone = DynamicNOAACalculatorTestCase.timeZone

return formatter
}()

private static var timeFormatter: DateFormatter = {
let formatter = DateFormatter()

formatter.setLocalizedDateFormatFromTemplate("hh:mm:ssa")
formatter.timeZone = DynamicNOAACalculatorTestCase.timeZone

return formatter
}()

/// A class method which returns dynamic tests as a dictionary.
/// The keys are the test names and the values are blocks to execute them.
/// - Returns: A dictionary of tests to invoke.
override class func dynamicTests() -> [String : @convention(block) () -> Void]? {

let gregorian: Calendar = .init(identifier: .gregorian)
var output: [String : @convention(block) () -> Void] = [:]

/// Load the tests from a CSV file.
guard let tests: [[String]] = try? TestsFromCSV.loadTests(from: "SunriseSunsetLakewoodNOAA") else {
XCTFail("Could not read tests from CSV.")
return [:]
}

/// This is the CSV headers, also the selectors to test.
guard let methodsToTest: [String] = tests.first else {
XCTFail("CSV was empty.")
return [:]
}

/// Iterate the test cases, skipping the headers row.
for testIdx in stride(from: 1, through: tests.count, by: 9) {
let testCase = tests[testIdx]
/// The first field is the date.
guard let inputDateAsString: String = testCase.first else {
XCTFail("Missing date in test case: \(testCase)")
continue
}

/// Iterate each method for the given date.
for (methodIdx, methodName) in (methodsToTest[1...]).enumerated() {

let dateFormattedForTest: String = inputDateAsString
.replacingOccurrences(of: ",", with: "")
.replacingOccurrences(of: " ", with: "_")

let selectorName: String = "test_\(methodName)_\(dateFormattedForTest)"

output[selectorName] = {
guard let workingDate = DynamicNOAACalculatorTestCase.dateFormatter.date(from: inputDateAsString) else {
XCTFail("Could not parse input date string. (Got \(inputDateAsString)).")
return
}

calendar.workingDate = workingDate

let selector = Selector(methodName)
guard let performedSelector = calendar.perform(selector) else {
XCTFail("Failed to call calendar.perform(Selector())")
return
}

guard let result: Date = performedSelector.takeUnretainedValue() as? Date else {
XCTFail("Result wasn't a date.")
return
}


/// As of iOS 17, the `DateFormatter` uses a different unicode value to
/// represent a space in time strings than it did before. This causes strings
/// created from dates to have this unexpected "narrow non-breaking-space"
/// instead of the character produced by using the space bar on your keyboard.
///
/// To work around this, we simply strip out all whitespace from both the test
/// result and the expected result. This simplifies comparing time strings that
/// have the form "<hour>:<minute>:<second><space><am or pm>".
///
/// Further reading:
/// https://developer.apple.com/forums/thread/731850
/// https://stackoverflow.com/questions/31272561/31483262#31483262
guard let expectedTime = DynamicNOAACalculatorTestCase.timeFormatter.date(from:testCase[methodIdx+1]) else {
XCTFail("Failed to parse date.")
return
}

/// Set the expected time on the input date - so we can compare the times on
/// the expected date.
let expectedComponents = gregorian.dateComponents([.hour, .minute, .second], from: expectedTime)
guard let expected = gregorian.date(
bySettingHour: expectedComponents.hour!,
minute: expectedComponents.minute!,
second: expectedComponents.second!,
of: workingDate
) else {
XCTFail("Failed to construct expected result date.")
return
}
/// Compare the resulting points in time, allowing for a 1 second discrepancy.
XCTAssertEqual(
result.timeIntervalSinceReferenceDate,
expected.timeIntervalSinceReferenceDate,
accuracy: 1
)
}
}
}

return output
}
}
27 changes: 27 additions & 0 deletions KosherCocoaTests/DynamicTestCase.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// DynamicTestCase.h
// DynamicTests
//
// Created by Moshe Berman on 12/31/23.
//

#ifndef DynamicTestCase_h
#define DynamicTestCase_h

#import <XCTest/XCTest.h>

/// A typedef for a block that neither accepts nor returns arguments.
typedef void(^TestBodyBlock)(void);


/// A class that can generate test cases at runtime.
@interface DynamicTestCase : XCTestCase

/// Override the implementation of this class method to define your custom tests.
/// Return a dictionary with the tests represented as blocks.
/// The name of each test is the dictionary key for the block.
+ (NSDictionary <NSString *, TestBodyBlock> *) dynamicTests;

@end

#endif /* DynamicTestCase_h */
71 changes: 71 additions & 0 deletions KosherCocoaTests/DynamicTestCase.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//
// DynamicTestCase.m
// DynamicTestCase
//
// Created by Moshe Berman on 12/29/23.
// Copyright © 2023 Moshe Berman. All rights reserved.
//

#import <XCTest/XCTest.h>
#import <objc/runtime.h>

#import "DynamicTestCase.h"

@implementation DynamicTestCase


/// Registers a block as a method for the supplied name.
/// - Parameters:
/// - name: The name of the new method.
/// - block: The block that represents the body of the method.
+ (void)addMethodNamed:(NSString *)name usingBlock:(void(^)(void))block {
SEL selForTestCase = NSSelectorFromString(name);
IMP implementation = imp_implementationWithBlock(^(id tc) {
block();
});

class_addMethod(self, selForTestCase, implementation, "v:@");
}


/// Converts a string into an invocation that points to a method.
/// - Parameter selectorString: The string representation of the selector.
+ (NSInvocation *)invocationFromSelectorNamed:(NSString *)selectorString {
SEL selector = NSSelectorFromString(selectorString);
NSMethodSignature *signature = [self instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.selector = selector;
return invocation;
}


/// Overrides XCTestCase's class method.
/// This implementation iterates a dictionary produced by`dynamicTests`and transforms it into test
/// invocations.
+ (NSArray<NSInvocation *> *)testInvocations {
NSMutableArray<NSInvocation *> *invocations = [NSMutableArray array];
for (NSString *key in self.dynamicTests) {
[self addMethodNamed:key usingBlock:self.dynamicTests[key]];
[invocations addObject:[self invocationFromSelectorNamed:key]];
}

return [invocations arrayByAddingObjectsFromArray:super.testInvocations];
}

/// Provides a hook for generating tests at runtime.
/// - Returns `NSDictionary<NSString *, void(^)(void)> *` a dictionary where the keys are selector names and the values are blocks.
/// Each block is a test case.
+ (NSDictionary <NSString *, TestBodyBlock> *) dynamicTests {
return @{
@"testThatDefaultTestFailsUnlessOverridden":^{
NSString *reason = [NSString stringWithFormat:@"You must override `%@` in a subclass.", NSStringFromSelector(_cmd)];
XCTExpectFailure(reason);
XCTFail(@"%@", reason);
},
@"passingTest": ^{
XCTAssertTrue(YES);
}
};
}

@end
Loading