Compare commits
2
Commits
main
...
issue-171-fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2161b3ae14 | ||
|
|
5fc26057d7 |
@@ -14,7 +14,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 50
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Check runner tools
|
- name: Check runner tools
|
||||||
run: |
|
run: |
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 50
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Check runner tools
|
- name: Check runner tools
|
||||||
run: |
|
run: |
|
||||||
@@ -73,6 +73,34 @@ jobs:
|
|||||||
DAGGER_NO_NAG: "1"
|
DAGGER_NO_NAG: "1"
|
||||||
run: task publish-android
|
run: task publish-android
|
||||||
|
|
||||||
|
- name: Cleanup TLS credentials
|
||||||
|
if: always()
|
||||||
|
run: rm -rf /tmp/dagger-tls /tmp/stunnel-dagger.conf /tmp/stunnel.pid
|
||||||
|
|
||||||
|
deploy-apk:
|
||||||
|
name: Build & Deploy APK to Server
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Check runner tools
|
||||||
|
run: |
|
||||||
|
command -v dagger >/dev/null 2>&1 || { echo "ERROR: dagger is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; }
|
||||||
|
command -v task >/dev/null 2>&1 || { echo "ERROR: task is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; }
|
||||||
|
dpkg -s stunnel4 netcat-openbsd >/dev/null 2>&1 || { echo "ERROR: stunnel4/netcat-openbsd are not installed in the runner image. Add them to .forgejo/Dockerfile."; exit 1; }
|
||||||
|
|
||||||
|
- name: Setup Dagger Remote Engine (via stunnel)
|
||||||
|
env:
|
||||||
|
DAGGER_STUNNEL_URL: ${{ secrets.DAGGER_STUNNEL_URL }}
|
||||||
|
DAGGER_CA_CERT: ${{ secrets.DAGGER_CA_CERT }}
|
||||||
|
DAGGER_CLIENT_CERT: ${{ secrets.DAGGER_CLIENT_CERT }}
|
||||||
|
DAGGER_CLIENT_KEY: ${{ secrets.DAGGER_CLIENT_KEY }}
|
||||||
|
run: scripts/setup_dagger_remote.sh
|
||||||
|
|
||||||
- name: Build & Deploy APK to server
|
- name: Build & Deploy APK to server
|
||||||
# continue-on-error: step requires SSH_PRIVATE_KEY secret; if unset the task
|
# continue-on-error: step requires SSH_PRIVATE_KEY secret; if unset the task
|
||||||
# precondition fails, but we don't want that to fail the whole job — the Play
|
# precondition fails, but we don't want that to fail the whole job — the Play
|
||||||
@@ -100,7 +128,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 50
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Check runner tools
|
- name: Check runner tools
|
||||||
run: |
|
run: |
|
||||||
@@ -137,16 +165,16 @@ jobs:
|
|||||||
publish-website:
|
publish-website:
|
||||||
name: Publish Website Build History
|
name: Publish Website Build History
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build-linux, deploy-playstore]
|
needs: [build-linux, deploy-playstore, deploy-apk]
|
||||||
if: |
|
if: |
|
||||||
always() &&
|
always() &&
|
||||||
(needs.build-linux.result == 'success' || needs.deploy-playstore.result == 'success')
|
(needs.build-linux.result == 'success' || needs.deploy-playstore.result == 'success' || needs.deploy-apk.result == 'success')
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 50
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Check runner tools
|
- name: Check runner tools
|
||||||
run: |
|
run: |
|
||||||
@@ -180,7 +208,7 @@ jobs:
|
|||||||
label-deploy-health:
|
label-deploy-health:
|
||||||
name: Update Deploy Health Label
|
name: Update Deploy Health Label
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [test-android-firebase, deploy-playstore, build-linux]
|
needs: [test-android-firebase, deploy-playstore, deploy-apk, build-linux]
|
||||||
if: always() && vars.DEPLOY_HEALTH_ISSUE != ''
|
if: always() && vars.DEPLOY_HEALTH_ISSUE != ''
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
|
|
||||||
@@ -190,7 +218,7 @@ jobs:
|
|||||||
FORGEJO_TOKEN: ${{ github.token }}
|
FORGEJO_TOKEN: ${{ github.token }}
|
||||||
FORGEJO_URL: ${{ github.server_url }}
|
FORGEJO_URL: ${{ github.server_url }}
|
||||||
DEPLOY_HEALTH_ISSUE: ${{ vars.DEPLOY_HEALTH_ISSUE }}
|
DEPLOY_HEALTH_ISSUE: ${{ vars.DEPLOY_HEALTH_ISSUE }}
|
||||||
ALL_SUCCEEDED: ${{ needs.test-android-firebase.result == 'success' && needs.deploy-playstore.result == 'success' && needs.build-linux.result == 'success' }}
|
ALL_SUCCEEDED: ${{ needs.test-android-firebase.result == 'success' && needs.deploy-playstore.result == 'success' && needs.deploy-apk.result == 'success' && needs.build-linux.result == 'success' }}
|
||||||
run: |
|
run: |
|
||||||
python3 - << 'PYEOF'
|
python3 - << 'PYEOF'
|
||||||
import os, json, urllib.request, urllib.error
|
import os, json, urllib.request, urllib.error
|
||||||
|
|||||||
@@ -616,6 +616,11 @@ Future<String> _resolveDatabasePath() async {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These two functions are only called from unit tests (database_path_test.dart).
|
||||||
|
// They expose internals that cannot be reached via the public API.
|
||||||
|
Future<String> resolveDatabasePathForTesting() => _resolveDatabasePath();
|
||||||
|
void resetDatabasePathForTesting() => _dbPath = null;
|
||||||
|
|
||||||
LazyDatabase _openConnection() {
|
LazyDatabase _openConnection() {
|
||||||
return LazyDatabase(() async {
|
return LazyDatabase(() async {
|
||||||
final file = File(await _resolveDatabasePath());
|
final file = File(await _resolveDatabasePath());
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:fake_async/fake_async.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
||||||
@@ -19,6 +22,30 @@ class _UnavailablePathProvider extends Fake
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fake PathProviderPlatform that fails the first [failCount] calls, then
|
||||||
|
// returns a fixed path. Used to exercise the retry loop in
|
||||||
|
// _resolveDatabasePath() without waiting for real timers.
|
||||||
|
class _SucceedAfterNPathProvider extends Fake
|
||||||
|
with MockPlatformInterfaceMixin
|
||||||
|
implements PathProviderPlatform {
|
||||||
|
_SucceedAfterNPathProvider({required this.failCount});
|
||||||
|
|
||||||
|
final int failCount;
|
||||||
|
int _callCount = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> getApplicationSupportPath() async {
|
||||||
|
_callCount++;
|
||||||
|
if (_callCount <= failCount) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: 'channel-error',
|
||||||
|
message: 'Simulated: path_provider channel not ready',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return '/tmp/test_app_support';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
@@ -38,4 +65,69 @@ void main() {
|
|||||||
await expectLater(initDatabasePath(), completes);
|
await expectLater(initDatabasePath(), completes);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Tests for _resolveDatabasePath() — the lazy retry path called on first DB
|
||||||
|
// access when initDatabasePath() already failed. fake_async lets us advance
|
||||||
|
// the back-off timers without waiting real-world milliseconds.
|
||||||
|
|
||||||
|
test(
|
||||||
|
'_resolveDatabasePath retries and eventually succeeds after transient failures',
|
||||||
|
() {
|
||||||
|
resetDatabasePathForTesting();
|
||||||
|
final prev = PathProviderPlatform.instance;
|
||||||
|
// Fail 3 times, succeed on the 4th call. The delays in
|
||||||
|
// _resolveDatabasePath are [200, 500, 1000, 2000, 4000] ms, so three
|
||||||
|
// failures cost 200+500+1000 = 1700 ms before the fourth attempt.
|
||||||
|
PathProviderPlatform.instance = _SucceedAfterNPathProvider(failCount: 3);
|
||||||
|
addTearDown(() {
|
||||||
|
PathProviderPlatform.instance = prev;
|
||||||
|
resetDatabasePathForTesting();
|
||||||
|
});
|
||||||
|
|
||||||
|
fakeAsync((fake) {
|
||||||
|
String? result;
|
||||||
|
unawaited(resolveDatabasePathForTesting().then((r) => result = r));
|
||||||
|
|
||||||
|
// Advance fake time through the three back-off delays.
|
||||||
|
fake.elapse(const Duration(milliseconds: 200 + 500 + 1000 + 1));
|
||||||
|
|
||||||
|
expect(result, isNotNull);
|
||||||
|
expect(result, endsWith('sharedinbox.db'));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'_resolveDatabasePath throws PlatformException after exhausting all retries',
|
||||||
|
() {
|
||||||
|
resetDatabasePathForTesting();
|
||||||
|
final prev = PathProviderPlatform.instance;
|
||||||
|
PathProviderPlatform.instance = _UnavailablePathProvider();
|
||||||
|
addTearDown(() {
|
||||||
|
PathProviderPlatform.instance = prev;
|
||||||
|
resetDatabasePathForTesting();
|
||||||
|
});
|
||||||
|
|
||||||
|
fakeAsync((fake) {
|
||||||
|
Object? caughtError;
|
||||||
|
unawaited(
|
||||||
|
resolveDatabasePathForTesting().catchError((Object e) {
|
||||||
|
caughtError = e;
|
||||||
|
return ''; // ignored; satisfies the Future<String> return type
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Advance past all five back-off delays: 200+500+1000+2000+4000 ms.
|
||||||
|
fake.elapse(
|
||||||
|
const Duration(milliseconds: 200 + 500 + 1000 + 2000 + 4000 + 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(caughtError, isA<PlatformException>());
|
||||||
|
expect(
|
||||||
|
(caughtError! as PlatformException).message,
|
||||||
|
contains('cannot open database'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user