From f596cbcaf266c8ea18a68203fbd624975ff4d04f Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Wed, 7 Feb 2024 02:59:51 -0500 Subject: [PATCH] Refactor & Fix Tests (#224) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * begin reworking test suites * dedicated hooks dir, eslint gha fix attempt * gha updates, fix lint issues from #211 * useRunTests hook working, still fighting mocha suites/tests * oops * consistent naming * fix android * update podfile.lock * fixed random,hash tests and upgraded chai * bump cocoapods version * bump GHA ruby version * install gems * more GHA/ios build fun * fix basic sign/verify test * comment out failing public cipher & sign/verify, fix generateKeyPair tests * cleanup * cpp lint * more github actions updates * finally the big re-work of tests * js lint * Test Results screen changes * docs and cleanup * better emojis for github * add/use test fixtures from Node.js * fix pbkdf2 deriveBits tests * quote github actions output correctly, duh * always more stragglers during self-review * ugh, more coffee --------- Co-authored-by: Szymon Kapała --- .github/workflows/build-android.yml | 10 +- .github/workflows/build-ios.yml | 17 +- .github/workflows/validate-android.yml | 12 +- .github/workflows/validate-cpp.yml | 2 +- .github/workflows/validate-js.yml | 12 +- android/CMakeLists.txt | 8 +- android/build.gradle | 5 +- cpp/Cipher/MGLGenerateKeyPairInstaller.cpp | 26 +- cpp/Hash/MGLHashHostObject.cpp | 2 +- cpp/Hash/MGLHashInstaller.cpp | 6 +- cpp/MGLKeys.cpp | 2 +- cpp/MGLQuickCryptoHostObject.cpp | 6 + cpp/Random/MGLRandomHostObject.cpp | 2 - cpp/Utils/MGLUtils.cpp | 2 +- cpp/Utils/MGLUtils.h | 1 + cpp/webcrypto/MGLWebCrypto.cpp | 4 +- example/.bundle/config | 4 +- example/android/app/build.gradle | 2 +- .../android/app/src/main/AndroidManifest.xml | 1 - example/android/build.gradle | 9 +- example/ios/Podfile.lock | 2 +- example/package.json | 6 +- example/src/components/Indentator.tsx | 30 -- example/src/components/TestItem.tsx | 69 ++- example/src/hooks/useRunTests.ts | 138 ++++++ example/src/hooks/useTestList.ts | 69 +++ example/src/navigators/Root.tsx | 6 + .../src/navigators/children/Entry/Entry.tsx | 97 ++-- .../navigators/children/Entry/TestItemType.ts | 5 - .../children/TestingScreen/RowItemType.ts | 7 - .../children/TestingScreen/TestingScreen.tsx | 149 +++--- .../TestingScreen/TestingScreenProps.ts | 5 +- example/src/testing/MochaRNAdapter.ts | 17 - example/src/testing/MochaSetup.ts | 75 --- example/src/testing/TestList.ts | 70 --- .../Tests/CipherTests/CipherTestFirst.ts | 10 +- .../Tests/CipherTests/CipherTestSecond.ts | 10 +- .../Tests/CipherTests/GenerateKeyPairTests.ts | 56 +-- .../Tests/CipherTests/PublicCipherTests.ts | 40 +- .../ConstantsTests.ts} | 12 +- .../src/testing/Tests/HashTests/HashTests.ts | 222 +++++---- .../src/testing/Tests/HmacTests/HmacTests.ts | 53 +-- .../RandomTests/{random.ts => randomTests.ts} | 340 +++++++------ .../src/testing/Tests/SignTests/SignTests.ts | 54 ++- .../src/testing/Tests/pbkdf2Tests/fixtures.ts | 61 +++ .../testing/Tests/pbkdf2Tests/pbkdf2Tests.ts | 447 +++++++++--------- .../Tests/webcryptoTests/webcryptoTests.ts | 106 +++-- example/src/types/TestResults.ts | 26 + example/src/types/TestSuite.ts | 8 + example/yarn.lock | 68 ++- implementation-coverage.md | 181 +++++++ src/Hashnames.ts | 32 +- src/keys.ts | 2 +- src/pbkdf2.ts | 14 +- src/random.ts | 3 + test/hashnames.test.ts | 10 +- 56 files changed, 1490 insertions(+), 1143 deletions(-) delete mode 100644 example/src/components/Indentator.tsx create mode 100644 example/src/hooks/useRunTests.ts create mode 100644 example/src/hooks/useTestList.ts delete mode 100644 example/src/navigators/children/Entry/TestItemType.ts delete mode 100644 example/src/navigators/children/TestingScreen/RowItemType.ts delete mode 100644 example/src/testing/MochaSetup.ts delete mode 100644 example/src/testing/TestList.ts rename example/src/testing/Tests/{ConstantsTest/ConstantsTest.ts => ConstantsTests/ConstantsTests.ts} (93%) rename example/src/testing/Tests/RandomTests/{random.ts => randomTests.ts} (71%) create mode 100644 example/src/types/TestResults.ts create mode 100644 example/src/types/TestSuite.ts create mode 100644 implementation-coverage.md diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index b78cec76..4a8b1daf 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -23,10 +23,10 @@ jobs: name: Build Android Example App runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: zulu java-version: 11 @@ -34,10 +34,10 @@ jobs: - name: Get yarn cache directory path id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Restore node_modules from cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -50,7 +50,7 @@ jobs: run: yarn install --frozen-lockfile --cwd example # - name: Restore Gradle cache - # uses: actions/cache@v2 + # uses: actions/cache@v4 # with: # path: | # ~/.gradle/caches diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index 3b024f8a..d5fb6cc6 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -24,13 +24,13 @@ jobs: run: working-directory: example/ios steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Get yarn cache directory path id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Restore node_modules from cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -41,18 +41,18 @@ jobs: run: yarn install --frozen-lockfile --cwd .. - name: Restore buildcache - uses: mikehardy/buildcache-action@v1 + uses: mikehardy/buildcache-action@v2 continue-on-error: true - name: Setup Ruby (bundle) uses: ruby/setup-ruby@v1 with: - ruby-version: 2.6 + ruby-version: 3.3 bundler-cache: true working-directory: example/ios - name: Restore Pods cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: | example/ios/Pods @@ -61,8 +61,11 @@ jobs: key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} restore-keys: | ${{ runner.os }}-pods- + - name: Install Gems + working-directory: example + run: bundle config set deployment 'true' && bundle install - name: Install Pods - run: bundle exec pod check || bundle exec pod install + run: bundle exec pod install - name: Install xcpretty run: gem install xcpretty - name: Build App diff --git a/.github/workflows/validate-android.yml b/.github/workflows/validate-android.yml index fe9c68ad..20a8a227 100644 --- a/.github/workflows/validate-android.yml +++ b/.github/workflows/validate-android.yml @@ -22,10 +22,10 @@ jobs: run: working-directory: ./android steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: zulu java-version: 11 @@ -33,9 +33,9 @@ jobs: - name: Get yarn cache directory path id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Restore node_modules from cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -48,7 +48,7 @@ jobs: run: yarn install --frozen-lockfile --cwd ../example # - name: Restore Gradle cache - # uses: actions/cache@v2 + # uses: actions/cache@v4 # with: # path: | # ~/.gradle/caches @@ -69,7 +69,7 @@ jobs: # name: Kotlin Lint # runs-on: ubuntu-latest # steps: - # - uses: actions/checkout@v2 + # - uses: actions/checkout@v4 # - name: Run KTLint # uses: mrousavy/action-ktlint@v1.7 # with: diff --git a/.github/workflows/validate-cpp.yml b/.github/workflows/validate-cpp.yml index d1f206da..75a88823 100644 --- a/.github/workflows/validate-cpp.yml +++ b/.github/workflows/validate-cpp.yml @@ -19,7 +19,7 @@ jobs: name: cpplint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: reviewdog/action-cpplint@master with: github_token: ${{ secrets.github_token }} diff --git a/.github/workflows/validate-js.yml b/.github/workflows/validate-js.yml index e886f225..cd8d7406 100644 --- a/.github/workflows/validate-js.yml +++ b/.github/workflows/validate-js.yml @@ -33,16 +33,16 @@ jobs: name: Compile JS (tsc) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install reviewdog uses: reviewdog/action-setup@v1 - name: Get yarn cache directory path id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Restore node_modules from cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -71,13 +71,13 @@ jobs: name: Lint JS (eslint, prettier) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Get yarn cache directory path id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Restore node_modules from cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index a680fbc9..b672fc41 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -1,13 +1,11 @@ project(react-native-quick-crypto) -cmake_minimum_required(VERSION 3.9.0) +cmake_minimum_required(VERSION 3.10.2) set(PACKAGE_NAME "reactnativequickcrypto") set(BUILD_DIR ${CMAKE_SOURCE_DIR}/build) -set(CMAKE_CXX_STANDARD 17) -# TODO(osp) remove before release -# set (CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g") -# set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g") +set (CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g") +set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g") # Consume shared libraries and headers from prefabs find_package(fbjni REQUIRED CONFIG) diff --git a/android/build.gradle b/android/build.gradle index 8b71eca3..ddbf15f1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -69,7 +69,10 @@ android { cmake { cppFlags "-O2 -frtti -fexceptions -Wall -fstack-protector-all -DON_ANDROID -DANDROID" abiFilters "x86", "x86_64", "armeabi-v7a", "arm64-v8a" - arguments '-DANDROID_STL=c++_shared' + arguments ( + '-DANDROID_STL=c++_shared', + "-DANDROID_TOOLCHAIN=clang" + ) } } } diff --git a/cpp/Cipher/MGLGenerateKeyPairInstaller.cpp b/cpp/Cipher/MGLGenerateKeyPairInstaller.cpp index 45ce55bb..1f36a78a 100644 --- a/cpp/Cipher/MGLGenerateKeyPairInstaller.cpp +++ b/cpp/Cipher/MGLGenerateKeyPairInstaller.cpp @@ -46,7 +46,7 @@ FieldDefinition getGenerateKeyPairFieldDefinition( runtime, jsi::Function::createFromHostFunction( runtime, jsi::PropNameID::forAscii(runtime, "executor"), 2, - [arguments, &jsCallInvoker, config]( + [&jsCallInvoker, config]( jsi::Runtime &runtime, const jsi::Value &, const jsi::Value *promiseArgs, size_t) -> jsi::Value { auto resolve = @@ -54,27 +54,33 @@ FieldDefinition getGenerateKeyPairFieldDefinition( auto reject = std::make_shared(runtime, promiseArgs[1]); - std::thread t([&runtime, arguments, resolve, reject, + std::thread t([&runtime, resolve, reject, jsCallInvoker, config]() { m.lock(); try { - auto keys = generateRSAKeyPair(runtime, config); - jsCallInvoker->invokeAsync([&runtime, &keys, jsCallInvoker, - resolve]() { + jsCallInvoker->invokeAsync([&runtime, config, resolve]() { + auto keys = generateRSAKeyPair(runtime, config); auto publicKey = toJSI(runtime, keys.first); auto privateKey = toJSI(runtime, keys.second); auto res = jsi::Array::createWithElements( - runtime, jsi::Value::undefined(), publicKey, - privateKey); + runtime, + jsi::Value::undefined(), + publicKey, + privateKey); resolve->asObject(runtime).asFunction(runtime).call( runtime, std::move(res)); }); } catch (std::exception e) { jsCallInvoker->invokeAsync( - [&runtime, &jsCallInvoker, reject]() { + [&runtime, reject]() { + auto res = jsi::Array::createWithElements( + runtime, + jsi::String::createFromUtf8( + runtime, "Error generating key"), + jsi::Value::undefined(), + jsi::Value::undefined()); reject->asObject(runtime).asFunction(runtime).call( - runtime, jsi::String::createFromUtf8( - runtime, "Error generating key")); + runtime, std::move(res)); }); } m.unlock(); diff --git a/cpp/Hash/MGLHashHostObject.cpp b/cpp/Hash/MGLHashHostObject.cpp index 7d2750ee..69492df2 100644 --- a/cpp/Hash/MGLHashHostObject.cpp +++ b/cpp/Hash/MGLHashHostObject.cpp @@ -67,7 +67,7 @@ void MGLHashHostObject::installMethods() { if (!arguments[0].isObject() || !arguments[0].getObject(runtime).isArrayBuffer(runtime)) { throw jsi::JSError(runtime, - "HmacHostObject::update: First argument ('message') " + "HashHostObject::update: First argument ('message') " "has to be of type ArrayBuffer!"); } auto messageBuffer = diff --git a/cpp/Hash/MGLHashInstaller.cpp b/cpp/Hash/MGLHashInstaller.cpp index 91b79b70..c8b57402 100644 --- a/cpp/Hash/MGLHashInstaller.cpp +++ b/cpp/Hash/MGLHashInstaller.cpp @@ -1,5 +1,5 @@ // -// HMAC-JSI-Installer.m +// Hash-JSI-Installer.m // PinkPanda // // Created by Marc Rousavy on 31.10.21. @@ -7,8 +7,6 @@ #include "MGLHashInstaller.h" -#include - #include #ifdef ANDROID @@ -29,7 +27,7 @@ FieldDefinition getHashFieldDefinition( // createHash(hashAlgorithm: 'sha1' | 'sha256' | 'sha512') return HOST_LAMBDA("createHash", { if (count != 1 && count != 2) { - throw jsi::JSError(runtime, "createHmac(..) expects 1-2 arguments!"); + throw jsi::JSError(runtime, "createHash(..) expects 1-2 arguments!"); } auto hashAlgorithm = arguments[0].asString(runtime).utf8(runtime); diff --git a/cpp/MGLKeys.cpp b/cpp/MGLKeys.cpp index 03423ec5..2a6ef38c 100644 --- a/cpp/MGLKeys.cpp +++ b/cpp/MGLKeys.cpp @@ -986,7 +986,7 @@ jsi::Value KeyObjectHandle::Init(jsi::Runtime &rt) { break; } default: - throw jsi::JSError(rt, "invalid keytype for init(): " + type); + throw jsi::JSError(rt, "invalid keytype for init(): " + std::to_string(type)); } return true; diff --git a/cpp/MGLQuickCryptoHostObject.cpp b/cpp/MGLQuickCryptoHostObject.cpp index 231ec0be..3d55f263 100644 --- a/cpp/MGLQuickCryptoHostObject.cpp +++ b/cpp/MGLQuickCryptoHostObject.cpp @@ -107,6 +107,12 @@ MGLQuickCryptoHostObject::MGLQuickCryptoHostObject( return jsi::Object::createFromHostObject(runtime, hostObject); })); + // createSign + this->fields.push_back(getSignFieldDefinition(jsCallInvoker, workerQueue)); + + // createVerify + this->fields.push_back(getVerifyFieldDefinition(jsCallInvoker, workerQueue)); + // subtle API created from a simple jsi::Object // because this FieldDefinition is only good for returning // objects and too convoluted diff --git a/cpp/Random/MGLRandomHostObject.cpp b/cpp/Random/MGLRandomHostObject.cpp index 7231938f..939e0e6e 100644 --- a/cpp/Random/MGLRandomHostObject.cpp +++ b/cpp/Random/MGLRandomHostObject.cpp @@ -47,7 +47,6 @@ MGLRandomHostObject::MGLRandomHostObject( } auto result = arguments[0].asObject(runtime).getArrayBuffer(runtime); - auto resultSize = result.size(runtime); auto *resultData = result.data(runtime); auto resultPreventGC = std::make_shared(std::move(result)); @@ -81,7 +80,6 @@ MGLRandomHostObject::MGLRandomHostObject( } auto result = arguments[0].asObject(runtime).getArrayBuffer(runtime); - auto resultSize = result.size(runtime); auto *resultData = result.data(runtime); auto offset = (int)arguments[1].asNumber(); auto size = arguments[2].asNumber(); diff --git a/cpp/Utils/MGLUtils.cpp b/cpp/Utils/MGLUtils.cpp index cc206ce2..49f5f442 100644 --- a/cpp/Utils/MGLUtils.cpp +++ b/cpp/Utils/MGLUtils.cpp @@ -30,7 +30,7 @@ jsi::Value toJSI(jsi::Runtime& rt, OptionJSVariant& value) { jsi::Value toJSI(jsi::Runtime& rt, JSVariant& value) { if (std::holds_alternative(value)) { - return std::get(value); + return jsi::Value(std::get(value)); } else if (std::holds_alternative(value)) { return jsi::Value(std::get(value)); } else if (std::holds_alternative(value)) { diff --git a/cpp/Utils/MGLUtils.h b/cpp/Utils/MGLUtils.h index e35b9cfa..925bd87b 100644 --- a/cpp/Utils/MGLUtils.h +++ b/cpp/Utils/MGLUtils.h @@ -19,6 +19,7 @@ #include #include #include +#include namespace margelo { diff --git a/cpp/webcrypto/MGLWebCrypto.cpp b/cpp/webcrypto/MGLWebCrypto.cpp index 38b4d0ce..518046bb 100644 --- a/cpp/webcrypto/MGLWebCrypto.cpp +++ b/cpp/webcrypto/MGLWebCrypto.cpp @@ -10,11 +10,13 @@ #include #include #include "MGLKeys.h" -#include "MGLUtils.h" + #ifdef ANDROID #include "JSIUtils/MGLJSIMacros.h" #include "webcrypto/crypto_ec.h" +#include "Utils/MGLUtils.h" #else +#include "MGLUtils.h" #include "MGLJSIMacros.h" #include "crypto_ec.h" #endif diff --git a/example/.bundle/config b/example/.bundle/config index 848943bb..eb725d9f 100644 --- a/example/.bundle/config +++ b/example/.bundle/config @@ -1,2 +1,4 @@ +--- BUNDLE_PATH: "vendor/bundle" -BUNDLE_FORCE_RUBY_PLATFORM: 1 +BUNDLE_FORCE_RUBY_PLATFORM: "1" +BUNDLE_DEPLOYMENT: "true" diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index d28df6ce..f2b0612f 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -48,7 +48,7 @@ dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") - implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0") + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 4122f36a..2f7cbbb7 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -7,7 +7,6 @@ android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" - android:allowBackup="false" android:theme="@style/AppTheme"> = ({ - indentation, - children, -}: IndentatorProps) => { - return ( - - - {children} - - ); -}; - -const styles = StyleSheet.create({ - container: { - width: '100%', - flexDirection: 'row', - alignContent: 'center', - }, - result: { - flex: 1, - }, -}); diff --git a/example/src/components/TestItem.tsx b/example/src/components/TestItem.tsx index 60f1ba05..959e7e73 100644 --- a/example/src/components/TestItem.tsx +++ b/example/src/components/TestItem.tsx @@ -1,29 +1,67 @@ import React from 'react'; -import { View, Text, StyleSheet } from 'react-native'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; import Checkbox from '@react-native-community/checkbox'; +import type { TestResult } from '../types/TestResults'; +import { useNavigation } from '@react-navigation/native'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import type { RootStackParamList } from '../navigators/RootProps'; type TestItemProps = { description: string; value: boolean; - index: number; - onToggle: (index: number) => void; + count: number; + results: TestResult[]; + onToggle: (description: string) => void; }; export const TestItem: React.FC = ({ description, value, - index, + count, + results, onToggle, }: TestItemProps) => { + const navigation = + useNavigation>(); + + // get pass/fail stats from results + let pass = 0; + let fail = 0; + results.map((r) => { + if (r.type === 'correct') pass++; + if (r.type === 'incorrect') fail++; + }); + return ( { - onToggle(index); + onToggle(description); }} /> - {description} + { + navigation.navigate('TestingScreen', { + results, + suiteName: description, + }); + }} + > + + {description} + + + {pass || ''} + + + {fail || ''} + + + {count} + + ); }; @@ -36,12 +74,27 @@ const styles = StyleSheet.create({ alignContent: 'center', alignItems: 'center', justifyContent: 'space-evenly', - marginTop: 10, - gap: 20, + gap: 10, borderBottomWidth: 1, borderBottomColor: '#ccc', }, label: { + fontSize: 12, + flex: 8, + }, + touchable: { + flex: 1, + flexDirection: 'row', + }, + pass: { + color: 'green', + }, + fail: { + color: 'red', + }, + count: { + fontSize: 12, flex: 1, + textAlign: 'right', }, }); diff --git a/example/src/hooks/useRunTests.ts b/example/src/hooks/useRunTests.ts new file mode 100644 index 00000000..db5f817f --- /dev/null +++ b/example/src/hooks/useRunTests.ts @@ -0,0 +1,138 @@ +import 'mocha'; +import type * as MochaTypes from 'mocha'; +import { useCallback, useState } from 'react'; +import type { Suites } from '../types/TestSuite'; +import type { Stats, SuiteResults, TestResult } from '../types/TestResults'; +import { rootSuite } from '../testing/MochaRNAdapter'; + +const defaultStats = { + start: new Date(), + end: new Date(), + duration: 0, + suites: 0, + tests: 0, + passes: 0, + pending: 0, + failures: 0, +}; + +export const useRunTests = (): [SuiteResults, (suites: Suites) => void] => { + const [results, setResults] = useState({}); + + const addResult = useCallback( + (newResult: TestResult) => { + setResults((prev) => { + if (!prev[newResult.suiteName]) { + prev[newResult.suiteName] = { results: [] }; + } + prev[newResult.suiteName]?.results.push(newResult); + return { ...prev }; + }); + }, + [setResults] + ); + + const runTests = (suites: Suites) => { + setResults({}); + run(addResult, suites); + }; + + return [results, runTests]; +}; + +const run = ( + addTestResult: (testResult: TestResult) => void, + tests: Suites = {} +) => { + const { + EVENT_RUN_BEGIN, + EVENT_RUN_END, + EVENT_TEST_FAIL, + EVENT_TEST_PASS, + EVENT_TEST_PENDING, + EVENT_TEST_END, + EVENT_SUITE_BEGIN, + EVENT_SUITE_END, + } = Mocha.Runner.constants; + + let stats: Stats = { ...defaultStats }; + + var runner = new Mocha.Runner(rootSuite) as MochaTypes.Runner; + runner.stats = stats; + + // enable/disable tests based on checkbox value + runner.suite.suites.map((s) => { + const suiteName = s.title; + if (!tests[suiteName]?.value) { + // console.log(`skipping '${suiteName}' suite`); + s.tests.map((t) => { + try { + t.skip(); + } catch (e) {} // do nothing w error + }); + } else { + // console.log(`will run '${suiteName}' suite`); + s.tests.map((t) => { + // @ts-expect-error - not sure why this is erroring + t.reset(); + }); + } + }); + + let indents = -1; + const indent = () => Array(indents).join(' '); + runner + .once(EVENT_RUN_BEGIN, () => { + stats.start = new Date(); + }) + .on(EVENT_SUITE_BEGIN, (suite: MochaTypes.Suite) => { + suite.root || stats.suites++; + indents++; + }) + .on(EVENT_SUITE_END, () => { + indents--; + }) + .on(EVENT_TEST_PASS, (test: MochaTypes.Runnable) => { + const name = test.parent?.title || ''; + stats.passes++; + addTestResult({ + indentation: indents, + description: test.fullTitle(), + suiteName: name, + type: 'correct', + }); + console.log(`${indent()}pass: ${test.fullTitle()}`); + }) + .on(EVENT_TEST_FAIL, (test: MochaTypes.Runnable, err: Error) => { + const name = test.parent?.title || ''; + stats.failures++; + addTestResult({ + indentation: indents, + description: test.fullTitle(), + suiteName: name, + type: 'incorrect', + errorMsg: err.message, + }); + console.log( + `${indent()}fail: ${test.fullTitle()} - error: ${err.message}` + ); + }) + .on(EVENT_TEST_PENDING, function () { + stats.pending++; + }) + .on(EVENT_TEST_END, function () { + stats.tests++; + }) + .once(EVENT_RUN_END, () => { + stats.end = new Date(); + stats.duration = stats.end.valueOf() - stats.start.valueOf(); + console.log(JSON.stringify(runner.stats, null, 2)); + }); + + runner.run(); + + return () => { + console.log('aborting'); + runner.abort(); + }; +}; diff --git a/example/src/hooks/useTestList.ts b/example/src/hooks/useTestList.ts new file mode 100644 index 00000000..053bc4c9 --- /dev/null +++ b/example/src/hooks/useTestList.ts @@ -0,0 +1,69 @@ +/* eslint-disable @typescript-eslint/no-shadow */ +import { useState, useCallback } from 'react'; +import type * as MochaTypes from 'mocha'; +import type { Suites } from '../types/TestSuite'; +import { rootSuite } from '../testing/MochaRNAdapter'; + +import '../testing/Tests/pbkdf2Tests/pbkdf2Tests'; +import '../testing/Tests/RandomTests/randomTests'; +import '../testing/Tests/HmacTests/HmacTests'; +import '../testing/Tests/HashTests/HashTests'; +import '../testing/Tests/CipherTests/CipherTestFirst'; +import '../testing/Tests/CipherTests/CipherTestSecond'; +import '../testing/Tests/CipherTests/PublicCipherTests'; +import '../testing/Tests/CipherTests/GenerateKeyPairTests'; +import '../testing/Tests/ConstantsTests/ConstantsTests'; +import '../testing/Tests/SignTests/SignTests'; +import '../testing/Tests/webcryptoTests/webcryptoTests'; + +export const useTestList = (): [ + Suites, + (description: string) => void, + () => void, + () => void +] => { + const [suites, setSuites] = useState(getInitialSuites); + + const toggle = useCallback( + (description: string) => { + setSuites((tests) => { + tests[description]!.value = !tests[description]!.value; + return tests; + }); + }, + [setSuites] + ); + + const clearAll = useCallback(() => { + setSuites((suites) => { + Object.entries(suites).forEach(([_, suite]) => { + suite.value = false; + }); + return { ...suites }; + }); + }, [setSuites]); + + const checkAll = useCallback(() => { + setSuites((suites) => { + Object.entries(suites).forEach(([_, suite]) => { + suite.value = true; + }); + return { ...suites }; + }); + }, [setSuites]); + + return [suites, toggle, clearAll, checkAll]; +}; + +const getInitialSuites = () => { + let suites: Suites = {}; + + // interrogate the loaded mocha suites/tests via a temporary runner + const runner = new Mocha.Runner(rootSuite) as MochaTypes.Runner; + runner.suite.suites.map((s) => { + suites[s.title] = { value: false, count: s.total() }; + }); + + // return count-enhanced list and totals + return suites; +}; diff --git a/example/src/navigators/Root.tsx b/example/src/navigators/Root.tsx index d11f9086..0dae99d5 100644 --- a/example/src/navigators/Root.tsx +++ b/example/src/navigators/Root.tsx @@ -11,6 +11,9 @@ export const Root: React.FC = () => { { const { Entry } = require('./children/Entry/Entry'); return Entry; @@ -25,6 +28,9 @@ export const Root: React.FC = () => { /> { const { TestingScreen, diff --git a/example/src/navigators/children/Entry/Entry.tsx b/example/src/navigators/children/Entry/Entry.tsx index f05d6c41..c6a18eb5 100644 --- a/example/src/navigators/children/Entry/Entry.tsx +++ b/example/src/navigators/children/Entry/Entry.tsx @@ -1,92 +1,59 @@ -/* eslint-disable @typescript-eslint/no-shadow */ -import React, { useState, useCallback } from 'react'; +import React from 'react'; import type { RootStackParamList } from '../../RootProps'; import type { NativeStackNavigationProp, NativeStackScreenProps, } from '@react-navigation/native-stack'; -import { View, ScrollView, StyleSheet, SafeAreaView } from 'react-native'; +import { Text, View, ScrollView, StyleSheet, SafeAreaView } from 'react-native'; +import 'mocha'; import { Button } from '../../../components/Button'; import { useNavigation } from '@react-navigation/native'; import { TestItem } from '../../../components/TestItem'; -import type { TestItemType } from './TestItemType'; -import { TEST_LIST } from '../../../testing/TestList'; +import { useTestList } from '../../../hooks/useTestList'; +import { useRunTests } from '../../../hooks/useRunTests'; type EntryProps = NativeStackScreenProps; -const useTests = (): [ - Array, - (index: number) => void, - () => void, - () => void -] => { - const [tests, setTests] = useState>(TEST_LIST); - - const toggle = useCallback( - (index: number) => { - setTests((tests) => { - tests[index]!.value = !tests[index]!.value; - return [...tests]; - }); - }, - [setTests] - ); - - const clearAll = useCallback(() => { - setTests((tests) => { - return tests.map((it) => { - it.value = false; - return it; - }); - }); - }, [setTests]); - - const checkAll = useCallback(() => { - setTests((tests) => { - return tests.map((it) => { - it.value = true; - return it; - }); - }); - }, [setTests]); - - return [tests, toggle, clearAll, checkAll]; -}; - export const Entry: React.FC = ({}: EntryProps) => { - const [tests, toggle, clearAll, checkAll] = useTests(); + const [tests, toggle, clearAll, checkAll] = useTestList(); + const [results, runTests] = useRunTests(); const navigation = useNavigation>(); + let totalCount = 0; + return ( - {tests.map((test, index: number) => ( - - ))} + {Object.entries(tests).map(([suiteName, suite], index) => { + totalCount += suite.count; + return ( + + ); + })} + + {totalCount} + -