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
4 changes: 3 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
- [ ] 🧪 Tests

## Checklist

<!-- Please make sure you've done the following before submitting your PR: -->

- [ ] Run `just precommit` to ensure that formatting and linting are correct
- [ ] Updated the `CHANGELOG.md` file with your changes
- [ ] Run `just check-flutter-coverage` to ensure that flutter coverage rules are passing
- [ ] Updated the `CHANGELOG.md` file with your changes (if they affect the user experience)
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Flutter
uses: subosito/flutter-action@v2
Expand Down Expand Up @@ -67,4 +69,10 @@ jobs:
run: flutter analyze --fatal-infos

- name: Run Flutter tests
run: flutter test
run: flutter test --coverage

- name: Install lcov
run: sudo apt-get install lcov

- name: Check coverage
run: ./scripts/check_diff_coverage.sh
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,6 @@ rust/.cargo/config.toml
# AI
**/.repomix-*
.goose/**

# Coverage
coverage/*
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,31 @@ just test-rust # Rust unit tests
just test-flutter # Flutter unit tests (when test/ exists)
```

### Coverage

You need to install lcov to generate report
```bash
# Mac OS
brew install lcov

# Linux
apt-get install lcov

# Windows
choco install lcov
```

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

show some love to windows 😅 they should get the lcov treatment too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OMG, I don't know how to install anything on windows 😓 ...

Also me right now:
Screenshot 2025-08-18 at 1 13 44 AM
...

Chatgpt says choco install lcov ... is this correct? Do you have a windows to check it out?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't own a windows myself which i probably should when we begin desktop rollout.
but after searching the internet i also found out choco works to install lcov.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so I'll add the choco 🍫 instructions haha

```bash
# Run tests with coverage and check diff coverage for changed files
just check-flutter-coverage

# Or run tests with coverage output manually
flutter test --coverage
# Generate coverage html report
genhtml coverage/lcov.info -o coverage/html
# Open coverage/html/index.html in your browser
```

### Cleaning

```bash
Expand Down
6 changes: 6 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ test-flutter:
@echo "🧪 Testing Flutter code..."
@if [ -d "test" ]; then flutter test; else echo "No test directory found. Create tests in test/ directory."; fi

# Test Flutter code with coverage and check diff coverage
check-flutter-coverage:
@echo "🧪 Testing Flutter code with coverage..."
flutter test --coverage
@echo "📊 Checking coverage for changed files..."
./scripts/check_diff_coverage.sh

# ==============================================================================
# CLEANING
Expand Down
4 changes: 0 additions & 4 deletions lib/config/extensions/toast_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,4 @@ extension ToastExtension on WidgetRef {
void setToastStackMode(ToastStackMode mode) {
read(toastMessageProvider.notifier).setStackMode(mode);
}

void setDefaultShowBelowAppBar(bool showBelowAppBar) {
read(toastMessageProvider.notifier).setDefaultShowBelowAppBar(showBelowAppBar);
}
}
15 changes: 9 additions & 6 deletions lib/config/providers/profile_ready_card_visibility_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import 'package:whitenoise/config/providers/active_account_provider.dart';
const String _profileReadyCardDismissedKey = 'profile_ready_card_dismissed';

class ProfileReadyCardVisibilityNotifier extends AsyncNotifier<bool> {
late final SharedPreferences _prefs;
ProfileReadyCardVisibilityNotifier({SharedPreferences? sharedPreferences})
: injectedSharedPreferences = sharedPreferences;

late SharedPreferences _sharedPreferences;
final SharedPreferences? injectedSharedPreferences;
String? _currentPubKey;

@override
Future<bool> build() async {
_prefs = await SharedPreferences.getInstance();
_sharedPreferences = injectedSharedPreferences ?? await SharedPreferences.getInstance();
final activeAccountPubkey = ref.watch(activeAccountProvider);
_currentPubKey = activeAccountPubkey;
return await _loadVisibilityState();
Expand All @@ -21,9 +25,8 @@ class ProfileReadyCardVisibilityNotifier extends AsyncNotifier<bool> {
if (_currentPubKey == null || _currentPubKey!.isEmpty) {
return true;
}

final isDismissed =
_prefs.getBool('${_profileReadyCardDismissedKey}_$_currentPubKey') ?? false;
_sharedPreferences.getBool('${_profileReadyCardDismissedKey}_$_currentPubKey') ?? false;
return !isDismissed;
} catch (e) {
return true;
Expand All @@ -37,7 +40,7 @@ class ProfileReadyCardVisibilityNotifier extends AsyncNotifier<bool> {
return;
}

await _prefs.setBool('${_profileReadyCardDismissedKey}_$_currentPubKey', true);
await _sharedPreferences.setBool('${_profileReadyCardDismissedKey}_$_currentPubKey', true);
state = const AsyncValue.data(false);
} catch (e) {
state = const AsyncValue.data(false);
Expand All @@ -51,7 +54,7 @@ class ProfileReadyCardVisibilityNotifier extends AsyncNotifier<bool> {
return;
}

await _prefs.remove('${_profileReadyCardDismissedKey}_$_currentPubKey');
await _sharedPreferences.remove('${_profileReadyCardDismissedKey}_$_currentPubKey');
state = const AsyncValue.data(true);
} catch (e) {
state = const AsyncValue.data(true);
Expand Down
13 changes: 8 additions & 5 deletions lib/config/providers/theme_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:whitenoise/config/states/theme_state.dart';

class ThemeNotifier extends Notifier<ThemeState> {
ThemeNotifier({SharedPreferences? prefs}) : _prefs = prefs;

final SharedPreferences? _prefs;
final _logger = Logger('ThemeNotifier');
static const String _themePreferenceKey = 'theme_mode';

@override
ThemeState build() {
Future.microtask(() => _loadThemeMode());
return const ThemeState();
}

final _logger = Logger('ThemeNotifier');
static const String _themePreferenceKey = 'theme_mode';

Future<void> _loadThemeMode() async {
try {
final prefs = await SharedPreferences.getInstance();
final prefs = _prefs ?? await SharedPreferences.getInstance();
final themeModeIndex = prefs.getInt(_themePreferenceKey);

if (themeModeIndex != null) {
Expand All @@ -33,7 +36,7 @@ class ThemeNotifier extends Notifier<ThemeState> {

Future<void> setThemeMode(ThemeMode themeMode) async {
try {
final prefs = await SharedPreferences.getInstance();
final prefs = _prefs ?? await SharedPreferences.getInstance();
await prefs.setInt(_themePreferenceKey, themeMode.index);
state = state.copyWith(themeMode: themeMode);
_logger.info('Theme mode set to: ${themeMode.name}');
Expand Down
10 changes: 0 additions & 10 deletions lib/config/providers/toast_message_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -212,22 +212,12 @@ class ToastMessageNotifier extends Notifier<ToastState> {
state = state.copyWith(messages: []);
}

void updateConfig(ToastConfig config) {
state = state.copyWith(config: config);
}

void setStackMode(ToastStackMode mode) {
state = state.copyWith(
config: state.config.copyWith(stackMode: mode),
);
}

void setDefaultShowBelowAppBar(bool showBelowAppBar) {
state = state.copyWith(
config: state.config.copyWith(defaultShowBelowAppBar: showBelowAppBar),
);
}

void _scheduleAutoDismiss(String id, int durationMs) {
Future.delayed(Duration(milliseconds: durationMs), () {
if (state.messages.any((msg) => msg.id == id)) {
Expand Down
197 changes: 197 additions & 0 deletions scripts/check_diff_coverage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#!/bin/bash
set -euo pipefail

MIN_COVERAGE=100
FILE_PATTERN="provider"
BASE_BRANCH="master"

print_error() {
red_color="\e[31;1m%s\e[0m\n"
printf "${red_color}" "$1"
}

print_success() {
green_color="\e[32;1m%s\e[0m\n"
printf "${green_color}" "$1"
}

raise_error() {
print_error "$1"
exit 1
}

check_prerequisites() {
local missing_tools
missing_tools=()

command -v git >/dev/null 2>&1 || missing_tools+=("git")
command -v lcov >/dev/null 2>&1 || missing_tools+=("lcov")
command -v bc >/dev/null 2>&1 || missing_tools+=("bc")

if [ ${#missing_tools[@]} -gt 0 ]; then
local tools_list
tools_list=$(printf ", %s" "${missing_tools[@]}")
tools_list=${tools_list:2} # Remove leading ", "
raise_error "Missing required tools: ${tools_list}. Please install them first."
fi
}

check_coverage_file_presence() {
if [ ! -f "coverage/lcov.info" ]; then
raise_error "Coverage file not found. Run 'flutter test --coverage' first."
fi
}

fetch_base_branch() {
local base_branch
base_branch=$1
printf "Fetching latest ${base_branch}...\n"
git fetch --no-tags --prune origin +refs/heads/${base_branch}:refs/remotes/origin/${base_branch}
}

get_merge_base() {
local base_branch
base_branch=$1
merge_base=$(git merge-base HEAD "origin/${base_branch}" 2>/dev/null || git merge-base HEAD "${base_branch}" 2>/dev/null || echo "")
echo "$merge_base"
}

check_merge_base() {
local base_branch
base_branch=$1
local merge_base
merge_base=$2
if [ -z "$merge_base" ]; then
raise_error "Could not find common ancestor with ${base_branch}. Ensure the branch exists and has shared history."
fi
}

get_changed_files() {
local merge_base
merge_base=$1
local file_pattern
file_pattern=$2
changed_files=$(git diff --name-only "$merge_base" HEAD | grep '^lib/.*\.dart$' | grep "${file_pattern}" || true)

if [ -z "$changed_files" ]; then
print_success "No lib/ Dart files matching '${file_pattern}' changed. Skipping coverage check."
exit 0
fi
printf "Changed files detected:\n%s\n" "$changed_files" >&2
echo "$changed_files"
}

make_temp_file() {
mktemp
}

calculate_coverage() {
local changed_files
changed_files=$1
local temp_lcov
temp_lcov=$2

# Convert newline-separated files to array to avoid word splitting issues
local -a files_array
while IFS= read -r file; do
files_array+=("$file")
done <<< "$changed_files"
lcov --extract coverage/lcov.info "${files_array[@]}" --output-file "$temp_lcov"
}

extract_coverage_percentage() {
local temp_lcov
temp_lcov=$1
local coverage_percentage
coverage_percentage="0"
local total_lines hit_lines
total_lines=$(grep "^LF:" "$temp_lcov" | awk -F: '{sum += $2} END {print sum}' || echo "0")
hit_lines=$(grep "^LH:" "$temp_lcov" | awk -F: '{sum += $2} END {print sum}' || echo "0")
if [ "$total_lines" -gt 0 ]; then
coverage_percentage=$(echo "scale=1; $hit_lines * 100 / $total_lines" | bc -l)
fi
coverage_percentage=$(printf "%.0f" "$coverage_percentage" 2>/dev/null || echo "0")
echo "$coverage_percentage"
}

print_files_with_low_coverage() {
local temp_lcov
temp_lcov=$1
local min_coverage
min_coverage=$2

lcov --quiet --list "$temp_lcov" 2>/dev/null | grep -v "Message summary" | grep -E "^[^|]*\|" | grep -v "Total:" | grep -v "Filename" | grep -v "====" | grep "\.dart" | while IFS='|' read -r file coverage rest; do
file_coverage=$(echo "$coverage" | sed 's/^[[:space:]]*\([0-9.]*\)%.*/\1/')
if [[ "$file_coverage" =~ ^[0-9]+\.?[0-9]*$ ]]; then
if [ "$(echo "$file_coverage < $min_coverage" | bc -l)" = "1" ]; then
print_error " - $file"
else
print_success " - $file"
fi
fi
done
}

handle_coverage_failure() {
local temp_lcov
temp_lcov=$1
local coverage_percentage
coverage_percentage=$2
local min_coverage
min_coverage=$3
print_error "Oops! Coverage ${coverage_percentage}% < ${min_coverage}% for changed files"
print_error "Check the coverage report for these files:"
print_files_with_low_coverage "$temp_lcov" "$min_coverage"
exit 1
}

check_coverage_result() {
local temp_lcov
temp_lcov=$1
local coverage_percentage
coverage_percentage=$2
local min_coverage
min_coverage=$3

if ! check_coverage_threshold "$coverage_percentage" "$min_coverage"; then
handle_coverage_failure "$temp_lcov" "$coverage_percentage" "$min_coverage"
fi

print_success "Coverage ${coverage_percentage}% ✓"
}

check_coverage_threshold() {
local coverage_percentage
coverage_percentage=$1
local min_coverage
min_coverage=$2
if [ -n "$coverage_percentage" ] && (($(echo "$coverage_percentage >= $min_coverage" | bc -l))); then
return 0
else
return 1
fi
}


check_diff_coverage() {
printf "Checking coverage for changed files...\n"
local base_branch
base_branch=$1
local file_pattern
file_pattern=$2
local min_coverage
min_coverage=$3
check_prerequisites
check_coverage_file_presence
fetch_base_branch "$base_branch"
merge_base=$(get_merge_base "$base_branch")
check_merge_base "$base_branch" "$merge_base"
changed_files=$(get_changed_files "$merge_base" "$file_pattern")
temp_file=$(make_temp_file)
trap "rm -f $temp_file" EXIT
calculate_coverage "$changed_files" "$temp_file"
coverage_percentage=$(extract_coverage_percentage "$temp_file")
check_coverage_result "$temp_file" "$coverage_percentage" "$min_coverage"
}

check_diff_coverage "$BASE_BRANCH" "$FILE_PATTERN" "$MIN_COVERAGE"
Loading