-
Notifications
You must be signed in to change notification settings - Fork 79
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Use the native ffi bindings for the Verifier instead of the rub…
…y bindings
- Loading branch information
1 parent
fc99e86
commit 119c3ce
Showing
17 changed files
with
1,157 additions
and
923 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// https://github.com/node-ffi/node-ffi/wiki/Node-FFI-Tutorial | ||
import ffi = require('ffi-napi'); | ||
|
||
// I am so so sorry about these types. They exist | ||
// to infer the returned type of the library | ||
// using the object that we pass in to describe the functions | ||
type AsyncFfiCall<Args extends unknown[], ReturnType> = { | ||
async: ( | ||
...args: [...Args, (err: Error, ret: ReturnType) => void] | ||
) => ReturnType; | ||
}; | ||
|
||
type FfiFunction<T> = T extends (...a: infer Args) => infer ReturnType | ||
? T & AsyncFfiCall<Args, ReturnType> | ||
: never; | ||
|
||
type StringType = 'string' | 'void' | 'int' | 'double' | 'float'; | ||
|
||
type ActualType<T> = [T] extends ['string'] | ||
? string | ||
: [T] extends ['void'] | ||
? void | ||
: [T] extends ['int' | 'double' | 'float'] | ||
? number | ||
: never; | ||
|
||
type ArrayActualType<Tuple extends [...Array<unknown>]> = { | ||
[Index in keyof Tuple]: ActualType<Tuple[Index]>; | ||
} & { length: Tuple['length'] }; | ||
|
||
type TupleType = [StringType, Array<StringType>]; | ||
|
||
type FunctionFromArray<A extends TupleType> = A extends [ | ||
r: infer ReturnTypeString, | ||
args: [...infer ArgArrayType] | ||
] | ||
? (...args: ArrayActualType<ArgArrayType>) => ActualType<ReturnTypeString> | ||
: never; | ||
|
||
type LibDescription<Functions extends string> = { | ||
[k in Functions]: [StringType, Array<StringType>]; | ||
}; | ||
|
||
type FfiBinding<T> = T extends LibDescription<string> | ||
? { | ||
[Key in keyof T]: FfiFunction<FunctionFromArray<T[Key]>>; | ||
} | ||
: never; | ||
|
||
// This function exists to wrap the untyped ffi lib, and return | ||
// the typed version | ||
export const initialiseFfi = <T>( | ||
binaryPath: string, | ||
description: T | ||
): FfiBinding<T> => | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
ffi.Library(binaryPath, description as { [k: string]: any }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { ArgMapping } from './types'; | ||
import logger from '../../logger'; | ||
|
||
export const argumentMapper = <PactOptions>( | ||
argMapping: ArgMapping<PactOptions>, | ||
options: PactOptions | ||
): string[] => | ||
Object.keys(options) | ||
.map((key: string) => { | ||
if (!argMapping[key]) { | ||
logger.error(`No argument mapping exists for '${key}'`); | ||
return []; | ||
} | ||
if (argMapping[key].warningMessage) { | ||
logger.warn(argMapping[key].warningMessage); | ||
return []; | ||
} | ||
if (argMapping[key].arg) { | ||
switch (argMapping[key].mapper) { | ||
case 'string': | ||
return [argMapping[key].arg, options[key]]; | ||
case 'flag': | ||
return options[key] ? [argMapping[key].arg] : []; | ||
default: | ||
logger.error( | ||
`Argument mapper for '${key}' maps to '${argMapping[key].arg}' with unknown mapper type '${argMapping[key].mapper}'` | ||
); | ||
return []; | ||
} | ||
} | ||
if (typeof argMapping[key] === 'function') { | ||
return argMapping[key](options[key]); | ||
} | ||
logger.error( | ||
`The argument mapper completely failed to find a mapping for '${key}'. This is a bug in pact-js-core` | ||
); | ||
return []; | ||
}) | ||
// This can be replaced with .flat() when node 10 is EOL | ||
.reduce((acc: string[], current: string[]) => [...acc, ...current], []); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
export type ArgMapping<PactOptions> = { | ||
[Key in keyof PactOptions]: | ||
| { | ||
arg: string; | ||
mapper: 'string' | 'flag'; | ||
} | ||
| { | ||
warningMessage: string; | ||
} | ||
| ((arg: NonNullable<PactOptions[Key]>) => string[]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import { LogLevel } from '../service'; | ||
import { ArgMapping } from './argumentMapper/types'; | ||
import { VerifierOptions } from './types'; | ||
|
||
import path = require('path'); | ||
import url = require('url'); | ||
import fs = require('fs'); | ||
|
||
type UriType = 'URL' | 'DIRECTORY' | 'FILE' | 'FILE_NOT_FOUND'; | ||
|
||
// Todo: Extract this, and possibly rename | ||
const fileType = (uri: string): UriType => { | ||
if (/https?:/.test(url.parse(uri).protocol || '')) { | ||
return 'URL'; | ||
} | ||
try { | ||
if (fs.statSync(path.normalize(uri)).isDirectory()) { | ||
return 'DIRECTORY'; | ||
} else { | ||
return 'FILE'; | ||
} | ||
} catch (e) { | ||
throw new Error(`Pact file or directory '${uri}' doesn't exist`); | ||
} | ||
}; | ||
|
||
export const argMapping: ArgMapping<VerifierOptions> = { | ||
providerBaseUrl: (providerBaseUrl: string) => { | ||
const u = url.parse(providerBaseUrl); | ||
return u && u.port && u.hostname | ||
? ['--port', u.port, '--hostname', u.hostname] | ||
: []; | ||
}, | ||
logLevel: (logLevel: LogLevel) => ['--loglevel', logLevel], | ||
provider: { arg: '--provider-name', mapper: 'string' }, | ||
pactUrls: (pactUrls: string[]) => | ||
pactUrls.reduce<Array<string>>((acc: Array<string>, uri: string) => { | ||
switch (fileType(uri)) { | ||
case 'URL': | ||
return [...acc, '--url', uri]; | ||
case 'DIRECTORY': | ||
return [...acc, '--dir', uri]; | ||
case 'FILE': | ||
return [...acc, '--file', uri]; | ||
case 'FILE_NOT_FOUND': | ||
throw new Error(`Pact file or directory '${uri}' doesn't exist`); | ||
default: | ||
return acc; | ||
} | ||
}, []), | ||
pactBrokerUrl: { arg: '--broker-url', mapper: 'string' }, | ||
pactBrokerUsername: { arg: '--user', mapper: 'string' }, | ||
pactBrokerPassword: { arg: '--password', mapper: 'string' }, | ||
pactBrokerToken: { arg: '--broker-token', mapper: 'string' }, | ||
consumerVersionTags: (tags: string | string[]) => [ | ||
'--consumer-version-tags', | ||
Array.isArray(tags) ? tags.join(',') : tags, | ||
], | ||
providerVersionTags: (tags: string | string[]) => [ | ||
'--provider-version-tags', | ||
Array.isArray(tags) ? tags.join(',') : tags, | ||
], | ||
providerStatesSetupUrl: { arg: '--state-change-url', mapper: 'string' }, | ||
|
||
providerVersion: { arg: '--provider-version', mapper: 'string' }, | ||
|
||
// Todo in FFI | ||
includeWipPactsSince: { arg: '--include-wip-pacts-since', mapper: 'string' }, | ||
consumerVersionSelectors: () => { | ||
throw new Error('Consumer version selectors are not yet implemented'); | ||
}, | ||
publishVerificationResult: { arg: '--publish', mapper: 'flag' }, | ||
enablePending: { arg: '--enable-pending', mapper: 'flag' }, | ||
|
||
// Todo in Rust | ||
customProviderHeaders: () => { | ||
throw new Error('customProviderHeaders are not yet implemented'); | ||
}, | ||
timeout: { | ||
warningMessage: 'Timeout currently has no effect on the rust binary', | ||
}, | ||
// We should support these, I think | ||
format: { | ||
warningMessage: | ||
"All output is currently on standard out, setting 'format' has no effect", | ||
}, | ||
out: { | ||
warningMessage: | ||
"All output is currently on standard out, setting 'out' has no effect", | ||
}, | ||
|
||
// Deprecate | ||
logDir: { | ||
warningMessage: | ||
'Setting logDir is deprecated as all logs are now on standard out.', | ||
}, | ||
verbose: { | ||
warningMessage: | ||
"Verbose mode is deprecated and has no effect, please use logLevel: 'DEBUG' instead", | ||
}, | ||
monkeypatch: { | ||
warningMessage: | ||
'The undocumented feature monkeypatch is no more, please file an issue if you were using it and need this functionality', | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import path = require('path'); | ||
import { initialiseFfi } from '../ffi/ffi'; | ||
|
||
// We have to declare this twice because typescript can't figure it out | ||
// There's a workaround here we could employ: | ||
// https://gist.github.com/jcalz/381562d282ebaa9b41217d1b31e2c211 | ||
type FfiVerifierType = { | ||
init: ['string', ['string']]; | ||
version: ['string', []]; | ||
free_string: ['void', ['string']]; | ||
verify: ['int', ['string']]; | ||
}; | ||
|
||
const description: FfiVerifierType = { | ||
init: ['string', ['string']], | ||
version: ['string', []], | ||
free_string: ['void', ['string']], | ||
verify: ['int', ['string']], | ||
}; | ||
|
||
// TODO: make this dynamic and select the right one for the architecture | ||
const dll = path.resolve( | ||
process.cwd(), | ||
'ffi', | ||
'libpact_verifier_ffi-osx-x86_64.dylib' | ||
); | ||
|
||
export const verifierLib = initialiseFfi(dll, description); |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import { VerifierOptions } from './types'; | ||
import { verifierLib } from './ffiVerifier'; | ||
import logger from '../logger'; | ||
import { argMapping } from './arguments'; | ||
import { argumentMapper } from './argumentMapper'; | ||
|
||
const VERIFICATION_SUCCESSFUL = 0; | ||
const VERIFICATION_FAILED = 1; | ||
// 2 - null string passed | ||
// 3 - method panicked | ||
const INVALID_ARGUMENTS = 4; | ||
|
||
const pactCrashMessage = ( | ||
extraMessage: string | ||
) => `!!!!!!!!! PACT CRASHED !!!!!!!!! | ||
${extraMessage} | ||
This is almost certainly a bug in pact-js-core. It would be great if you could | ||
open a bug report at: https://github.com/pact-foundation/pact-js-core/issues | ||
so that we can fix it. | ||
There is additional debugging information above. If you open a bug report, | ||
please rerun with logLevel: 'debug' set in the VerifierOptions, and include the | ||
full output. | ||
SECURITY WARNING: Before including your log in the issue tracker, make sure you | ||
have removed sensitive info such as login credentials and urls that you don't want | ||
to share with the world. | ||
Lastly, we're sorry about this! | ||
`; | ||
|
||
export const verify = (opts: VerifierOptions): Promise<string> => { | ||
// Todo: probably separate out the sections of this logic into separate promises | ||
return new Promise<string>((resolve, reject) => { | ||
// Todo: Does this need to be a specific log level? | ||
// PACT_LOG_LEVEL | ||
// LOG_LEVEL | ||
// < .. > | ||
verifierLib.init('LOG_LEVEL'); | ||
|
||
const request = argumentMapper(argMapping, opts).join('\n'); | ||
|
||
logger.debug('sending arguments to FFI:'); | ||
logger.debug(request); | ||
|
||
verifierLib.verify.async(request, (err: Error, res: number) => { | ||
logger.debug(`response from verifier: ${err}, ${res}`); | ||
if (err) { | ||
logger.error(err); | ||
logger.error( | ||
pactCrashMessage( | ||
'The underlying pact core returned an error through the ffi interface' | ||
) | ||
); | ||
reject(err); | ||
} else { | ||
switch (res) { | ||
case VERIFICATION_SUCCESSFUL: | ||
logger.info('Verification successful'); | ||
resolve(`finished: ${res}`); | ||
break; | ||
case VERIFICATION_FAILED: | ||
logger.error('Verification unsuccessful'); | ||
reject(new Error('Verfication failed')); | ||
break; | ||
case INVALID_ARGUMENTS: | ||
logger.error( | ||
pactCrashMessage( | ||
'The underlying pact core was invoked incorrectly.' | ||
) | ||
); | ||
reject(new Error('Verification was unable to run')); | ||
break; | ||
default: | ||
logger.error( | ||
pactCrashMessage( | ||
'The underlying pact core crashed in an unexpected way.' | ||
) | ||
); | ||
reject(new Error('Pact core crashed')); | ||
break; | ||
} | ||
} | ||
}); | ||
}); | ||
}; |
Oops, something went wrong.