Skip to content
Merged
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
76 changes: 76 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
# ── 1. Static analysis ────────────────────────────────────────────────────────
analyze:
name: Analyze
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: dart-lang/setup-dart@v1
with:
sdk: stable

- name: Install dependencies
working-directory: packages/fletch
run: dart pub get

- name: Analyze
working-directory: packages/fletch
run: dart analyze --fatal-infos

# ── 2. Tests + coverage enforcement ──────────────────────────────────────────
test:
name: Test & Coverage
needs: analyze
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: dart-lang/setup-dart@v1
with:
sdk: stable

- name: Install dependencies
working-directory: packages/fletch
run: dart pub get

- name: Run tests with coverage
working-directory: packages/fletch
run: dart test --coverage=coverage

- name: Format coverage to lcov
working-directory: packages/fletch
run: |
dart pub global activate coverage
dart pub global run coverage:format_coverage \
--lcov \
--in=coverage \
--out=coverage/lcov.info \
--packages=.dart_tool/package_config.json \
--report-on=lib

- name: Enforce coverage threshold (≥ 90%)
working-directory: packages/fletch
run: |
COVERAGE=$(awk -F: '/^LF/{lf+=$2} /^LH/{lh+=$2} END{printf "%.1f", lh*100/lf}' coverage/lcov.info)
echo "Coverage: ${COVERAGE}%"
if awk "BEGIN{exit !(${COVERAGE} < 90)}"; then
echo "::error::Coverage ${COVERAGE}% is below the required 90% threshold"
exit 1
fi
echo "Coverage ${COVERAGE}% meets the 90% threshold"

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: packages/fletch/coverage/lcov.info
fail_ci_if_error: false
75 changes: 75 additions & 0 deletions .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Mutation Testing

on:
# Run weekly on Monday at 03:00 UTC (off-peak)
schedule:
- cron: '0 3 * * 1'
# Allow manual trigger from the Actions tab
workflow_dispatch:
inputs:
sample:
description: 'Mutation sample size (0 = all)'
required: false
default: '50'
threshold:
description: 'Minimum mutation score (0-100)'
required: false
default: '75'

jobs:
mutate:
name: Mutation Score
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: dart-lang/setup-dart@v1
with:
sdk: stable

- name: Install dependencies
working-directory: packages/fletch
run: dart pub get

- name: Install dart_mutant
run: |
curl -fsSL \
"https://github.com/MelbourneDeveloper/dart_mutant/releases/download/v0.1.0/dart_mutant-v0.1.0-aarch64-apple-darwin.tar.gz" \
-o /tmp/dart_mutant.tar.gz || true
# Fallback: try x86_64 linux binary
curl -fsSL \
"https://github.com/MelbourneDeveloper/dart_mutant/releases/latest/download/dart_mutant-x86_64-unknown-linux-gnu.tar.gz" \
-o /tmp/dart_mutant.tar.gz
tar xz -C /usr/local/bin -f /tmp/dart_mutant.tar.gz
dart_mutant --version

- name: Run mutation testing
working-directory: packages/fletch
run: |
dart_mutant \
--glob "lib/**/*.dart" \
--exclude "**/*.g.dart" "benchmark/**" \
--parallel 4 \
--sample ${{ github.event.inputs.sample || '50' }} \
--threshold ${{ github.event.inputs.threshold || '75' }} \
--html \
--junit \
--ai-report \
--output ./mutation-reports

- name: Upload mutation report
if: always()
uses: actions/upload-artifact@v4
with:
name: mutation-report
path: packages/fletch/mutation-reports/
retention-days: 30

- name: Publish test results
if: always()
uses: mikepenz/action-junit-report@v4
with:
report_paths: packages/fletch/mutation-reports/*.xml
check_name: Mutation Test Results
fail_on_failure: false
22 changes: 20 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,27 @@ jobs:
working-directory: packages/fletch
run: dart analyze --fatal-infos

- name: Run Tests
- name: Run Tests with coverage
working-directory: packages/fletch
run: dart test
run: dart test --coverage=coverage

- name: Format coverage to lcov
working-directory: packages/fletch
run: |
dart pub global activate coverage
dart pub global run coverage:format_coverage \
--lcov \
--in=coverage \
--out=coverage/lcov.info \
--packages=.dart_tool/package_config.json \
--report-on=lib

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: packages/fletch/coverage/lcov.info
fail_ci_if_error: false

publish:
needs: test
Expand Down
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
fletch_copy/
fletch_copy/
packages/fletch/benchmark/dartmark/
coverage/
.idea/
.dart_tool/
apps/fletch_bench/drafts/
6 changes: 3 additions & 3 deletions packages/fletch/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ include: package:lints/recommended.yaml
# rules:
# - camel_case_types

# analyzer:
# exclude:
# - path/to/excluded/files/**
analyzer:
exclude:
- benchmark/**

# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints
Expand Down
7 changes: 4 additions & 3 deletions packages/fletch/benchmark/bin/test_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ void main() async {
{'status': 'healthy', 'timestamp': DateTime.now().toIso8601String()});
});

await app.listen(3005);
print('🚀 Benchmark server running on http://localhost:3000');
await app.listen(3008);
print('🚀 Benchmark server running on http://localhost:3008');
print('');
print('Available endpoints:');
print(' GET / - Simple hello message');
Expand All @@ -42,5 +42,6 @@ void main() async {
print(' GET /health - Health check');
print('');
print('Test with:');
print(' dart run bin/load_test.dart http://localhost:3000 1000 10');
print(' dart run bin/load_test.dart http://localhost:3008 1000 10');
}

12 changes: 12 additions & 0 deletions packages/fletch/lib/src/models/memory_session_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,21 @@ class MemorySessionStore implements SessionStore {
final Map<String, _SessionEntry> _sessions = {};
final Duration defaultTTL;
final Duration _cleanupInterval;
final int maxSessions;
Timer? _cleanupTimer;

/// Creates a memory-backed session store.
///
/// Parameters:
/// - [defaultTTL]: Default session lifetime (default: 24 hours)
/// - [cleanupInterval]: How often to clean expired sessions (default: 10 minutes)
/// - [maxSessions]: Maximum live sessions kept in memory (default: 10,000).
/// When the limit is reached, the oldest sessions are evicted to prevent
/// unbounded memory growth under sustained traffic.
MemorySessionStore({
this.defaultTTL = const Duration(hours: 24),
Duration cleanupInterval = const Duration(minutes: 10),
this.maxSessions = 10000,
}) : _cleanupInterval = cleanupInterval {
_startCleanup();
}
Expand Down Expand Up @@ -75,6 +80,13 @@ class MemorySessionStore implements SessionStore {
}) async {
final expiresAt = DateTime.now().add(ttl ?? defaultTTL);

// Evict oldest entries when at capacity (before inserting a new one).
if (!_sessions.containsKey(sessionId) &&
_sessions.length >= maxSessions) {
// Dart Maps are insertion-ordered: the first key is the oldest.
_sessions.remove(_sessions.keys.first);
}

_sessions[sessionId] = _SessionEntry(
data: Map<String, dynamic>.from(data), // Store a copy
expiresAt: expiresAt,
Expand Down
Loading