Add PromiseKit dependency

- Added PromiseKit dependency
This commit is contained in:
2018-11-15 22:08:00 -04:00
parent 2689d86c18
commit be7b6b5881
541 changed files with 46282 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
# From https://github.com/github/gitignore/blob/master/Node.gitignore
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next

View File

@@ -0,0 +1,80 @@
//
// AllTests.swift
// PMKJSA+Tests
//
// Created by Lois Di Qual on 2/28/18.
//
import XCTest
import PromiseKit
import JavaScriptCore
class AllTests: XCTestCase {
func testAll() {
let scriptPath = URL(fileURLWithPath: #file).deletingLastPathComponent().appendingPathComponent("build/build.js")
guard FileManager.default.fileExists(atPath: scriptPath.path) else {
return print("Skipping JS-A+: see README for instructions on how to build")
}
guard let script = try? String(contentsOf: scriptPath) else {
return XCTFail("Couldn't read content of test suite JS file")
}
let context = JSUtils.sharedContext
// Add a global exception handler
context.exceptionHandler = { context, exception in
guard let exception = exception else {
return XCTFail("Unknown JS exception")
}
JSUtils.printStackTrace(exception: exception, includeExceptionDescription: true)
}
// Setup mock functions (timers, console.log, etc)
let environment = MockNodeEnvironment()
environment.setup(with: context)
// Expose JSPromise in the javascript context
context.setObject(JSPromise.self, forKeyedSubscript: "JSPromise" as NSString)
// Create adapter
guard let adapter = JSValue(object: NSDictionary(), in: context) else {
fatalError("Couldn't create adapter")
}
adapter.setObject(JSAdapter.resolved, forKeyedSubscript: "resolved" as NSString)
adapter.setObject(JSAdapter.rejected, forKeyedSubscript: "rejected" as NSString)
adapter.setObject(JSAdapter.deferred, forKeyedSubscript: "deferred" as NSString)
// Evaluate contents of `build.js`, which exposes `runTests` in the global context
context.evaluateScript(script)
guard let runTests = context.objectForKeyedSubscript("runTests") else {
return XCTFail("Couldn't find `runTests` in JS context")
}
// Create a callback that's called whenever there's a failure
let onFail: @convention(block) (JSValue, JSValue) -> Void = { test, error in
guard let test = test.toString(), let error = error.toString() else {
return XCTFail("Unknown test failure")
}
XCTFail("\(test) failed: \(error)")
}
let onFailValue: JSValue = JSValue(object: onFail, in: context)
// Create a new callback that we'll send to `runTest` so that it notifies when tests are done running.
let expectation = self.expectation(description: "async")
let onDone: @convention(block) (JSValue) -> Void = { failures in
expectation.fulfill()
}
let onDoneValue: JSValue = JSValue(object: onDone, in: context)
// If there's a need to only run one specific test, uncomment the next line and comment the one after
// let testName: JSValue = JSValue(object: "2.3.1", in: context)
let testName = JSUtils.undefined
// Call `runTests`
runTests.call(withArguments: [adapter, onFailValue, onDoneValue, testName])
self.wait(for: [expectation], timeout: 60)
}
}

View File

@@ -0,0 +1,53 @@
//
// JSAdapter.swift
// PMKJSA+Tests
//
// Created by Lois Di Qual on 3/2/18.
//
import Foundation
import JavaScriptCore
import PromiseKit
enum JSAdapter {
static let resolved: @convention(block) (JSValue) -> JSPromise = { value in
return JSPromise(promise: .value(value))
}
static let rejected: @convention(block) (JSValue) -> JSPromise = { reason in
let error = JSUtils.JSError(reason: reason)
let promise = Promise<JSValue>(error: error)
return JSPromise(promise: promise)
}
static let deferred: @convention(block) () -> JSValue = {
let context = JSContext.current()
guard let object = JSValue(object: NSDictionary(), in: context) else {
fatalError("Couldn't create object")
}
let pendingPromise = Promise<JSValue>.pending()
let jsPromise = JSPromise(promise: pendingPromise.promise)
// promise
object.setObject(jsPromise, forKeyedSubscript: "promise" as NSString)
// resolve
let resolve: @convention(block) (JSValue) -> Void = { value in
pendingPromise.resolver.fulfill(value)
}
object.setObject(resolve, forKeyedSubscript: "resolve" as NSString)
// reject
let reject: @convention(block) (JSValue) -> Void = { reason in
let error = JSUtils.JSError(reason: reason)
pendingPromise.resolver.reject(error)
}
object.setObject(reject, forKeyedSubscript: "reject" as NSString)
return object
}
}

View File

@@ -0,0 +1,94 @@
//
// JSPromise.swift
// PMKJSA+Tests
//
// Created by Lois Di Qual on 3/1/18.
//
import Foundation
import XCTest
import PromiseKit
import JavaScriptCore
@objc protocol JSPromiseProtocol: JSExport {
func then(_: JSValue, _: JSValue) -> JSPromise
}
class JSPromise: NSObject, JSPromiseProtocol {
let promise: Promise<JSValue>
init(promise: Promise<JSValue>) {
self.promise = promise
}
func then(_ onFulfilled: JSValue, _ onRejected: JSValue) -> JSPromise {
// Keep a reference to the returned promise so we can comply to 2.3.1
var returnedPromiseRef: Promise<JSValue>?
let afterFulfill = promise.then { value -> Promise<JSValue> in
// 2.2.1: ignored if not a function
guard JSUtils.isFunction(value: onFulfilled) else {
return .value(value)
}
// Call `onFulfilled`
// 2.2.5: onFulfilled/onRejected must be called as functions (with no `this` value)
guard let returnValue = try JSUtils.call(function: onFulfilled, arguments: [JSUtils.undefined, value]) else {
return .value(value)
}
// Extract JSPromise.promise if available, or use plain return value
if let jsPromise = returnValue.toObjectOf(JSPromise.self) as? JSPromise {
// 2.3.1: if returned value is the promise that `then` returned, throw TypeError
if jsPromise.promise === returnedPromiseRef {
throw JSUtils.JSError(reason: JSUtils.typeError(message: "Returned self"))
}
return jsPromise.promise
} else {
return .value(returnValue)
}
}
let afterReject = promise.recover { error -> Promise<JSValue> in
// 2.2.1: ignored if not a function
guard let jsError = error as? JSUtils.JSError, JSUtils.isFunction(value: onRejected) else {
throw error
}
// Call `onRejected`
// 2.2.5: onFulfilled/onRejected must be called as functions (with no `this` value)
guard let returnValue = try JSUtils.call(function: onRejected, arguments: [JSUtils.undefined, jsError.reason]) else {
throw error
}
// Extract JSPromise.promise if available, or use plain return value
if let jsPromise = returnValue.toObjectOf(JSPromise.self) as? JSPromise {
// 2.3.1: if returned value is the promise that `then` returned, throw TypeError
if jsPromise.promise === returnedPromiseRef {
throw JSUtils.JSError(reason: JSUtils.typeError(message: "Returned self"))
}
return jsPromise.promise
} else {
return .value(returnValue)
}
}
let newPromise = Promise<Result<JSValue>> { resolver in
_ = promise.tap(resolver.fulfill)
}.then(on: nil) { result -> Promise<JSValue> in
switch result {
case .fulfilled: return afterFulfill
case .rejected: return afterReject
}
}
returnedPromiseRef = newPromise
return JSPromise(promise: newPromise)
}
}

View File

@@ -0,0 +1,116 @@
//
// JSUtils.swift
// PMKJSA+Tests
//
// Created by Lois Di Qual on 3/2/18.
//
import Foundation
import JavaScriptCore
enum JSUtils {
class JSError: Error {
let reason: JSValue
init(reason: JSValue) {
self.reason = reason
}
}
static let sharedContext: JSContext = {
guard let context = JSContext() else {
fatalError("Couldn't create JS context")
}
return context
}()
static var undefined: JSValue {
guard let undefined = JSValue(undefinedIn: JSUtils.sharedContext) else {
fatalError("Couldn't create `undefined` value")
}
return undefined
}
static func typeError(message: String) -> JSValue {
let message = message.replacingOccurrences(of: "\"", with: "\\\"")
let script = "new TypeError(\"\(message)\")"
guard let result = sharedContext.evaluateScript(script) else {
fatalError("Couldn't create TypeError")
}
return result
}
// @warning: relies on lodash to be present
static func isFunction(value: JSValue) -> Bool {
guard let context = value.context else {
return false
}
guard let lodash = context.objectForKeyedSubscript("_") else {
fatalError("Couldn't get lodash in JS context")
}
guard let result = lodash.invokeMethod("isFunction", withArguments: [value]) else {
fatalError("Couldn't invoke _.isFunction")
}
return result.toBool()
}
// Calls a JS function using `Function.prototype.call` and throws any potential exception wrapped in a JSError
static func call(function: JSValue, arguments: [JSValue]) throws -> JSValue? {
let context = JSUtils.sharedContext
// Create a new exception handler that will store a potential exception
// thrown in the handler. Save the value of the old handler.
var caughtException: JSValue?
let savedExceptionHandler = context.exceptionHandler
context.exceptionHandler = { context, exception in
caughtException = exception
}
// Call the handler
let returnValue = function.invokeMethod("call", withArguments: arguments)
context.exceptionHandler = savedExceptionHandler
// If an exception was caught, throw it
if let exception = caughtException {
throw JSError(reason: exception)
}
return returnValue
}
static func printCurrentStackTrace() {
guard let exception = JSUtils.sharedContext.evaluateScript("new Error()") else {
return print("Couldn't get current stack trace")
}
printStackTrace(exception: exception, includeExceptionDescription: false)
}
static func printStackTrace(exception: JSValue, includeExceptionDescription: Bool) {
guard let lineNumber = exception.objectForKeyedSubscript("line"),
let column = exception.objectForKeyedSubscript("column"),
let message = exception.objectForKeyedSubscript("message"),
let stacktrace = exception.objectForKeyedSubscript("stack")?.toString() else {
return print("Couldn't print stack trace")
}
if includeExceptionDescription {
print("JS Exception at \(lineNumber):\(column): \(message)")
}
let lines = stacktrace.split(separator: "\n").map { "\t> \($0)" }.joined(separator: "\n")
print(lines)
}
}
#if !swift(>=3.2)
extension String {
func split(separator: Character, omittingEmptySubsequences: Bool = true) -> [String] {
return characters.split(separator: separator, omittingEmptySubsequences: omittingEmptySubsequences).map(String.init)
}
var first: Character? {
return characters.first
}
}
#endif

View File

@@ -0,0 +1,117 @@
//
// MockNodeEnvironment.swift
// PMKJSA+Tests
//
// Created by Lois Di Qual on 3/1/18.
//
import Foundation
import JavaScriptCore
class MockNodeEnvironment {
private var timers: [UInt32: Timer] = [:]
func setup(with context: JSContext) {
// console.log / console.error
setupConsole(context: context)
// setTimeout
let setTimeout: @convention(block) (JSValue, Double) -> UInt32 = { function, intervalMs in
let timerID = self.addTimer(interval: intervalMs / 1000, repeats: false, function: function)
return timerID
}
context.setObject(setTimeout, forKeyedSubscript: "setTimeout" as NSString)
// clearTimeout
let clearTimeout: @convention(block) (JSValue) -> Void = { timeoutID in
guard timeoutID.isNumber else {
return
}
self.removeTimer(timerID: timeoutID.toUInt32())
}
context.setObject(clearTimeout, forKeyedSubscript: "clearTimeout" as NSString)
// setInterval
let setInterval: @convention(block) (JSValue, Double) -> UInt32 = { function, intervalMs in
let timerID = self.addTimer(interval: intervalMs / 1000, repeats: true, function: function)
return timerID
}
context.setObject(setInterval, forKeyedSubscript: "setInterval" as NSString)
// clearInterval
let clearInterval: @convention(block) (JSValue) -> Void = { intervalID in
guard intervalID.isNumber else {
return
}
self.removeTimer(timerID: intervalID.toUInt32())
}
context.setObject(clearInterval, forKeyedSubscript: "clearInterval" as NSString)
}
private func setupConsole(context: JSContext) {
guard let console = context.objectForKeyedSubscript("console") else {
fatalError("Couldn't get global `console` object")
}
let consoleLog: @convention(block) () -> Void = {
guard let arguments = JSContext.currentArguments(), let format = arguments.first as? JSValue else {
return
}
let otherArguments = arguments.dropFirst()
if otherArguments.count == 0 {
print(format)
} else {
let otherArguments = otherArguments.compactMap { $0 as? JSValue }
let format = format.toString().replacingOccurrences(of: "%s", with: "%@")
let expectedTypes = format.split(separator: "%", omittingEmptySubsequences: false).dropFirst().compactMap { $0.first }.map { String($0) }
let typedArguments = otherArguments.enumerated().compactMap { index, value -> CVarArg? in
let expectedType = expectedTypes[index]
let converted: CVarArg
switch expectedType {
case "s": converted = value.toString()
case "d": converted = value.toInt32()
case "f": converted = value.toDouble()
default: converted = value.toString()
}
return converted
}
let output = String(format: format, arguments: typedArguments)
print(output)
}
}
console.setObject(consoleLog, forKeyedSubscript: "log" as NSString)
console.setObject(consoleLog, forKeyedSubscript: "error" as NSString)
}
private func addTimer(interval: TimeInterval, repeats: Bool, function: JSValue) -> UInt32 {
let block = BlockOperation {
DispatchQueue.main.async {
function.call(withArguments: [])
}
}
let timer = Timer.scheduledTimer(timeInterval: interval, target: block, selector: #selector(Operation.main), userInfo: nil, repeats: repeats)
let rawHash = UUID().uuidString.hashValue
#if swift(>=4.0)
let hash = UInt32(truncatingIfNeeded: rawHash)
#else
let hash = UInt32(truncatingBitPattern: rawHash)
#endif
timers[hash] = timer
return hash
}
private func removeTimer(timerID: UInt32) {
guard let timer = timers[timerID] else {
return print("Couldn't find timer \(timerID)")
}
timer.invalidate()
timers[timerID] = nil
}
}

View File

@@ -0,0 +1,75 @@
Promises/A+ Compliance Test Suite (JavaScript)
==============================================
What is this?
-------------
This contains the necessary Swift and JS files to run the Promises/A+ compliance test suite from PromiseKit's unit tests.
- Promise/A+ Spec: <https://promisesaplus.com/>
- Compliance Test Suite: <https://github.com/promises-aplus/promises-tests>
Run tests
---------
```
$ npm install
$ npm run build
```
then open `PromiseKit.xcodeproj` and run the `PMKJSA+Tests` unit test scheme.
Known limitations
-----------------
See `ignoredTests` in `index.js`.
- 2.3.3 is disabled: Otherwise, if x is an object or function. This spec is a NOOP for Swift:
- We have decided not to interact with other Promises A+ implementations
- functions cannot have properties
Upgrade the test suite
----------------------
```
$ npm install --save promises-aplus-tests@latest
$ npm run build
```
Develop
-------
JavaScriptCore is a bit tedious to work with so here are a couple tips in case you're trying to debug the test suite.
If you're editing JS files, enable live rebuilds:
```
$ npm run watch
```
If you're editing Swift files, a couple things you can do:
- You can adjust `testName` in `AllTests.swift` to only run one test suite
- You can call `JSUtils.printCurrentStackTrace()` at any time. It won't contain line numbers but some of the frame names might help.
How it works
------------
The Promises/A+ test suite is written in JavaScript but PromiseKit is written in Swift/ObjC. For the test suite to run against swift code, we expose a promise wrapper `JSPromise` inside a JavaScriptCore context. This is done in a regular XCTestCase.
Since JavaScriptCore doesn't support CommonJS imports, we inline all the JavaScript code into `build/build.js` using webpack. This includes all the npm dependencies (`promises-aplus-tests`, `mocha`, `sinon`, etc) as well as the glue code in `index.js`.
`build.js` exposes one global variable `runTests(adapter, onFail, onDone, [testName])`. In our XCTestCase, a shared JavaScriptCore context is created, `build.js` is evaluated and now `runTests` is accessible from the Swift context.
In our swift test, we create a JS-bridged `JSPromise` which only has one method `then(onFulfilled, onRejected) -> Promise`. It wraps a swift `Promise` and delegates call `then` calls to it.
An [adapter](https://github.com/promises-aplus/promises-tests#adapters) plain JS object which provides `revoled(value), rejected(reason), and deferred()` is passed to `runTests` to run the whole JavaScript test suite.
Errors and end events are reported back to Swift and piped to `XCTFail()` if necessary.
Since JavaScriptCore isn't a node/web environment, there is quite a bit of stubbing necessary for all this to work:
- The `fs` module is stubbed with an empty function
- `console.log` redirects to `Swift.print` and provides only basic format parsing
- `setTimeout/setInterval` are implemented with `Swift.Timer` behind the scenes and stored in a `[TimerID: Timer]` map.

View File

@@ -0,0 +1,42 @@
const _ = require('lodash')
require('mocha')
// Ignored by design
const ignoredTests = [
'2.3.3'
]
module.exports = function(adapter, onFail, onDone, testName) {
global.adapter = adapter
const mocha = new Mocha({ ui: 'bdd' })
// Require all tests
console.log('Loading test files')
const requireTest = require.context('promises-aplus-tests/lib/tests', false, /\.js$/)
requireTest.keys().forEach(file => {
let currentTestName = _.replace(_.replace(file, './', ''), '.js', '')
if (testName && currentTestName !== testName) {
return
}
if (_.includes(ignoredTests, currentTestName)) {
return
}
console.log(`\t${currentTestName}`)
mocha.suite.emit('pre-require', global, file, mocha)
mocha.suite.emit('require', requireTest(file), file, mocha)
mocha.suite.emit('post-require', global, file, mocha)
})
const runner = mocha.run(failures => {
onDone(failures)
})
runner.on('fail', (test, err) => {
console.error(err)
onFail(test.title, err)
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
{
"scripts": {
"build": "webpack-cli",
"watch": "webpack-cli --watch --mode development"
},
"dependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.3",
"babel-preset-env": "^1.6.1",
"lodash": "^4.17.5",
"mocha": "^5.0.1",
"promises-aplus-tests": "^2.1.2",
"sinon": "^4.4.2",
"webpack": "^4.0.1",
"webpack-cli": "^2.0.9"
}
}

View File

@@ -0,0 +1,29 @@
var webpack = require('webpack');
module.exports = {
mode: 'development',
context: __dirname,
entry: './index.js',
output: {
path: __dirname + '/build',
filename: 'build.js',
library: 'runTests'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['env']
}
}
}
]
},
node: {
fs: 'empty'
},
};