Skip to content
Draft
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
108 changes: 108 additions & 0 deletions .github/actions/collect-devicefarm-results/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ outputs:
log-file:
description: Copied harness log file path
value: ${{ steps.find-harness-log.outputs.log_file }}
xcodebuild-log-file:
description: Copied iOS XCTest xcodebuild log file path
value: ${{ steps.find-xcodebuild-log.outputs.log_file }}
customer-artifacts-log-file:
description: Copied Device Farm customer artifacts log file path
value: ${{ steps.find-customer-artifacts-log.outputs.log_file }}
test-spec-output-file:
description: Copied Device Farm test spec output log file path
value: ${{ steps.find-test-spec-output.outputs.log_file }}
test-spec-timestamped-output-file:
description: Copied Device Farm timestamped test spec output log file path
value: ${{ steps.find-test-spec-timestamped-output.outputs.log_file }}

runs:
using: composite
Expand Down Expand Up @@ -51,6 +63,74 @@ runs:
"$WORKSPACE_LOG"
echo "log_file=$WORKSPACE_LOG" >> "$GITHUB_OUTPUT"

- name: Find Harness XCTest log
id: find-xcodebuild-log
if: inputs.platform == 'ios'
shell: bash
run: |
set -euo pipefail
ARTIFACT_DIR="${{ inputs.artifact-folder }}"
WORKSPACE_LOG=".github/test-results/harness-xcodebuild-ios.log"
if bash "$GITHUB_ACTION_PATH/../../scripts/find-devicefarm-artifact.sh" \
"$ARTIFACT_DIR" \
'xcodebuild.log' \
"$WORKSPACE_LOG"; then
echo "log_file=$WORKSPACE_LOG" >> "$GITHUB_OUTPUT"
else
echo "log_file=" >> "$GITHUB_OUTPUT"
fi

- name: Find Device Farm customer artifacts log
id: find-customer-artifacts-log
shell: bash
run: |
set -euo pipefail
ARTIFACT_DIR="${{ inputs.artifact-folder }}"
PLATFORM="${{ inputs.platform }}"
WORKSPACE_LOG=".github/test-results/devicefarm-customer-artifacts-${PLATFORM}.log"
if bash "$GITHUB_ACTION_PATH/../../scripts/find-devicefarm-artifact.sh" \
"$ARTIFACT_DIR" \
'*-Customer Artifacts Log.txt' \
"$WORKSPACE_LOG"; then
echo "log_file=$WORKSPACE_LOG" >> "$GITHUB_OUTPUT"
else
echo "log_file=" >> "$GITHUB_OUTPUT"
fi

- name: Find Device Farm test spec output
id: find-test-spec-output
shell: bash
run: |
set -euo pipefail
ARTIFACT_DIR="${{ inputs.artifact-folder }}"
PLATFORM="${{ inputs.platform }}"
WORKSPACE_LOG=".github/test-results/devicefarm-test-spec-output-${PLATFORM}.log"
if bash "$GITHUB_ACTION_PATH/../../scripts/find-devicefarm-artifact.sh" \
"$ARTIFACT_DIR" \
'*-Test spec output.txt' \
"$WORKSPACE_LOG"; then
echo "log_file=$WORKSPACE_LOG" >> "$GITHUB_OUTPUT"
else
echo "log_file=" >> "$GITHUB_OUTPUT"
fi

- name: Find Device Farm timestamped test spec output
id: find-test-spec-timestamped-output
shell: bash
run: |
set -euo pipefail
ARTIFACT_DIR="${{ inputs.artifact-folder }}"
PLATFORM="${{ inputs.platform }}"
WORKSPACE_LOG=".github/test-results/devicefarm-test-spec-timestamped-output-${PLATFORM}.log"
if bash "$GITHUB_ACTION_PATH/../../scripts/find-devicefarm-artifact.sh" \
"$ARTIFACT_DIR" \
'*-Test spec timestamped output.txt' \
"$WORKSPACE_LOG"; then
echo "log_file=$WORKSPACE_LOG" >> "$GITHUB_OUTPUT"
else
echo "log_file=" >> "$GITHUB_OUTPUT"
fi

- name: Publish Harness test results
id: publish-harness-results
if: steps.find-harness-junit.outputs.junit_file != ''
Expand All @@ -77,6 +157,34 @@ runs:
cat "$LOG_FILE"
echo "===== End Harness ${PLATFORM} output log ====="

XCODEBUILD_LOG="${{ steps.find-xcodebuild-log.outputs.log_file }}"
if [[ -n "$XCODEBUILD_LOG" && ( "$TEST_STEP_OUTCOME" == "failure" || "$PUBLISH_OUTCOME" == "failure" ) ]]; then
echo "===== Harness ${PLATFORM} xcodebuild log ====="
cat "$XCODEBUILD_LOG"
echo "===== End Harness ${PLATFORM} xcodebuild log ====="
fi

CUSTOMER_ARTIFACTS_LOG="${{ steps.find-customer-artifacts-log.outputs.log_file }}"
if [[ -n "$CUSTOMER_ARTIFACTS_LOG" && "$TEST_STEP_OUTCOME" == "failure" ]]; then
echo "===== Device Farm ${PLATFORM} customer artifacts log ====="
cat "$CUSTOMER_ARTIFACTS_LOG"
echo "===== End Device Farm ${PLATFORM} customer artifacts log ====="
fi

TEST_SPEC_OUTPUT="${{ steps.find-test-spec-output.outputs.log_file }}"
if [[ -n "$TEST_SPEC_OUTPUT" && "$TEST_STEP_OUTCOME" == "failure" ]]; then
echo "===== Device Farm ${PLATFORM} test spec output ====="
cat "$TEST_SPEC_OUTPUT"
echo "===== End Device Farm ${PLATFORM} test spec output ====="
fi

TEST_SPEC_TIMESTAMPED_OUTPUT="${{ steps.find-test-spec-timestamped-output.outputs.log_file }}"
if [[ -n "$TEST_SPEC_TIMESTAMPED_OUTPUT" && "$TEST_STEP_OUTCOME" == "failure" ]]; then
echo "===== Device Farm ${PLATFORM} timestamped test spec output ====="
cat "$TEST_SPEC_TIMESTAMPED_OUTPUT"
echo "===== End Device Farm ${PLATFORM} timestamped test spec output ====="
fi

if [[ "$PUBLISH_OUTCOME" == "failure" ]]; then
echo "Harness test result publication reported failures." >&2
exit 1
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/harness-aws-device.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ on:
- main
paths:
- ".github/workflows/harness-aws-device.yml"
- ".github/actions/collect-devicefarm-results/**"
- ".github/actions/upload-devicefarm-artifact/**"
- ".github/scripts/find-devicefarm-artifact.sh"
- "apps/simple-camera/**"
- "packages/react-native-vision-camera/**"
- "packages/react-native-vision-camera-barcode-scanner/**"
Expand All @@ -48,7 +50,9 @@ on:
pull_request:
paths:
- ".github/workflows/harness-aws-device.yml"
- ".github/actions/collect-devicefarm-results/**"
- ".github/actions/upload-devicefarm-artifact/**"
- ".github/scripts/find-devicefarm-artifact.sh"
- "apps/simple-camera/**"
- "packages/react-native-vision-camera/**"
- "packages/react-native-vision-camera-barcode-scanner/**"
Expand Down Expand Up @@ -118,7 +122,9 @@ jobs:
- '!packages/**/*.@(swift|mm)'
- 'patches/**'
- '.github/workflows/harness-aws-device.yml'
- '.github/actions/collect-devicefarm-results/**'
- '.github/actions/upload-devicefarm-artifact/**'
- '.github/scripts/find-devicefarm-artifact.sh'
- 'bun.lock'
- 'package.json'

Expand Down
5 changes: 4 additions & 1 deletion apps/simple-camera/__tests__/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ Tests are split by domain. Each file tests one slice of the imperative `VisionCa
| [visioncamera.frame.harness.ts](visioncamera.frame.harness.ts) | `createFrameOutput`, worklet install via `react-native-vision-camera-worklets`, YUV / RGB / native pixel formats, `scheduleOnRN`, `createSynchronizable`, `setOnFrameDroppedCallback`, `enablePreviewSizedOutputBuffers` |
| [visioncamera.multi-output.harness.ts](visioncamera.multi-output.harness.ts) | Multi-output sessions that combine photo, video, and frame outputs, output replacement while other outputs stay attached, persistent recording across session restarts |
| [visioncamera.constraints.harness.ts](visioncamera.constraints.harness.ts) | `VisionCamera.resolveConstraints` + `onSessionConfigSelected`, FPS / HDR / stabilization / binned / pixelFormat / resolutionBias constraints |
| [visioncamera.controller.harness.ts](visioncamera.controller.harness.ts) | `CameraController` — zoom, torch, exposure bias, focus metering, low-light boost, subject area listener |
| [visioncamera.controller.zoom.harness.ts](visioncamera.controller.zoom.harness.ts) | `CameraController` zoom |
| [visioncamera.controller.torch.harness.ts](visioncamera.controller.torch.harness.ts) | `CameraController` torch and torch strength |
| [visioncamera.controller.exposure.harness.ts](visioncamera.controller.exposure.harness.ts) | `CameraController` exposure bias and low-light boost |
| [visioncamera.controller.metering.harness.ts](visioncamera.controller.metering.harness.ts) | `CameraController` focus metering, focus / exposure / white-balance locking, subject area listener |
| [visioncamera.coordinates.harness.ts](visioncamera.coordinates.harness.ts) | `Frame.convertFramePointToCameraPoint` / `convertCameraPointToFramePoint`, `PreviewView.convertViewPointToCameraPoint` / `convertCameraPointToViewPoint`, `PreviewView.createMeteringPoint`, `convertScannedObjectCoordinatesToViewCoordinates`, end-to-end Frame → Camera → View round-trip |
| [visioncamera.nativepreviewview.harness.tsx](visioncamera.nativepreviewview.harness.tsx) | Bare `NativePreviewView` lifecycle, layout-sensitive preview regression coverage, `resizeMode`, Android `implementationMode`, gesture controllers, multi-preview mounting, `PreviewView` ref methods, Android `takeSnapshot()` dimensions |
| [visioncamera.camera-view.harness.tsx](visioncamera.camera-view.harness.tsx) | High-level `<Camera>` preview lifecycle, photo output integration, controller props, native gestures, `CameraRef` methods, `isActive`, mount / unmount / replacement behavior |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { beforeAll, describe, expect, it } from 'react-native-harness'
import type {
CameraDevice,
CameraDeviceFactory,
} from 'react-native-vision-camera'
import { CommonResolutions, VisionCamera } from 'react-native-vision-camera'

describe('VisionCamera - Controller Exposure', () => {
let factory: CameraDeviceFactory
let backDevice: CameraDevice

beforeAll(async () => {
await VisionCamera.requestCameraPermission()
expect(VisionCamera.cameraPermissionStatus).toBe('authorized')
factory = await VisionCamera.createDeviceFactory()
const back = factory.getDefaultCamera('back')
expect(back).toBeDefined()
if (back == null) throw new Error('no back camera')
backDevice = back
})

it('sets exposure bias to min/max when the device supports it', async (context) => {
if (!backDevice.supportsExposureBias) {
return context.skip('exposureBias: not supported on this device')
}
const session = await VisionCamera.createCameraSession(false)
const photoOutput = VisionCamera.createPhotoOutput({
targetResolution: CommonResolutions.HD_4_3,
containerFormat: 'jpeg',
quality: 0.8,
qualityPrioritization: 'balanced',
})
const [controller] = await session.configure([
{
input: backDevice,
outputs: [{ output: photoOutput, mirrorMode: 'auto' }],
constraints: [],
},
])
if (controller == null) throw new Error('no controller')
await session.start()

try {
// AVCaptureDevice quantizes the requested bias to a discrete rational
// step, so the readback may differ from the request by a tiny epsilon.
await controller.setExposureBias(backDevice.maxExposureBias)
expect(controller.exposureBias).toBeCloseTo(backDevice.maxExposureBias, 4)

await controller.setExposureBias(backDevice.minExposureBias)
expect(controller.exposureBias).toBeCloseTo(backDevice.minExposureBias, 4)

await controller.setExposureBias(0)
expect(controller.exposureBias).toBeCloseTo(0, 4)
} finally {
await session.stop()
}
})

it('rejects setExposureBias outside the device range', async (context) => {
if (!backDevice.supportsExposureBias) {
return context.skip(
'exposureBias out-of-range: not supported on this device',
)
}
const session = await VisionCamera.createCameraSession(false)
const photoOutput = VisionCamera.createPhotoOutput({
targetResolution: CommonResolutions.HD_4_3,
containerFormat: 'jpeg',
quality: 0.8,
qualityPrioritization: 'balanced',
})
const [controller] = await session.configure([
{
input: backDevice,
outputs: [{ output: photoOutput, mirrorMode: 'auto' }],
constraints: [],
},
])
if (controller == null) throw new Error('no controller')
await session.start()

try {
const tooHighExposureBias = backDevice.maxExposureBias + 1
const tooLowExposureBias = backDevice.minExposureBias - 1
await expect(
controller.setExposureBias(tooHighExposureBias),
).rejects.toThrow()
await expect(
controller.setExposureBias(tooLowExposureBias),
).rejects.toThrow()
} finally {
await session.stop()
}
})

// TODO(Android): Re-enable once initial CameraX control config is applied after the camera is active.
it.skip('honors initialExposureBias passed to configure', async (context) => {
if (!backDevice.supportsExposureBias) {
return context.skip(
'initialExposureBias: device does not support exposure bias',
)
}
const session = await VisionCamera.createCameraSession(false)
const photoOutput = VisionCamera.createPhotoOutput({
targetResolution: CommonResolutions.HD_4_3,
containerFormat: 'jpeg',
quality: 0.8,
qualityPrioritization: 'balanced',
})
const initial = backDevice.maxExposureBias
const [controller] = await session.configure([
{
input: backDevice,
outputs: [{ output: photoOutput, mirrorMode: 'auto' }],
constraints: [],
initialExposureBias: initial,
},
])
if (controller == null) throw new Error('no controller')
await session.start()

try {
expect(controller.exposureBias).toBeCloseTo(initial, 4)
} finally {
await session.stop()
}
})

it('enables low-light boost via CameraController.configure when supported', async (context) => {
const lowLightDevice = factory.cameraDevices.find(
(d) => d.supportsLowLightBoost,
)
if (lowLightDevice == null) {
return context.skip(
'low-light boost: no device on this system supports it',
)
}
const session = await VisionCamera.createCameraSession(false)
const photoOutput = VisionCamera.createPhotoOutput({
targetResolution: CommonResolutions.HD_4_3,
containerFormat: 'jpeg',
quality: 0.8,
qualityPrioritization: 'balanced',
})
const [controller] = await session.configure([
{
input: lowLightDevice,
outputs: [{ output: photoOutput, mirrorMode: 'auto' }],
constraints: [],
},
])
if (controller == null) throw new Error('no controller')
await session.start()

try {
await controller.configure({ enableLowLightBoost: true })
expect(controller.isLowLightBoostEnabled).toBe(true)
await controller.configure({ enableLowLightBoost: false })
expect(controller.isLowLightBoostEnabled).toBe(false)
} finally {
await session.stop()
}
})
})
Loading
Loading