Skip to content
Closed
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
28 changes: 28 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
reviews:
auto_review:
enabled: true
auto_incremental_review: true
auto_pause_after_reviewed_commits: 0
drafts: false
ignore_title_keywords:
- "WIP"
- "[skip review]"
ignore_usernames:
- "dependabot[bot]"
high_level_summary: true
high_level_summary_in_walkthrough: true
poem: false
review_status: true
collapse_walkthrough: false
path_instructions:
- path: "firmware/**"
instructions: |
This repo contains embedded firmware, app, server, and remote-control code. For firmware changes, prioritize memory usage, build reproducibility, hardware safety, transport compatibility, and regressions in BLE/Wi-Fi control paths.
- path: "server/**"
instructions: |
For server changes, prioritize request/response correctness, configuration safety, auth handling, and whether new code remains testable without requiring a live database.
- path: "app/**"
instructions: |
For Flutter app changes, prioritize BLE/device-control correctness, platform compatibility, analyzer cleanliness, and avoiding generated-file churn in reviews.
chat:
auto_reply: true
86 changes: 86 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: CI

on:
pull_request:
push:
branches:
- main

permissions:
contents: read
id-token: write

jobs:
server-tests:
name: Server tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: server
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: server/go.mod

- name: Run Go vet
run: go vet ./internal/model/...

- name: Run Staticcheck
run: |
go install honnef.co/go/tools/cmd/staticcheck@v0.6.1
"$(go env GOPATH)/bin/staticcheck" ./internal/model/...

- name: Run Go tests with coverage
run: go test -covermode=atomic -coverprofile=coverage.out ./internal/model/...

- name: Upload server coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: ./server/coverage.out
flags: server
name: server
fail_ci_if_error: true
use_oidc: true

app-tests:
name: App tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: app
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install native build dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake ninja-build pkg-config libgtk-3-dev liblzma-dev clang

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true

- name: Install dependencies
run: flutter pub get

- name: Run focused Flutter analysis
run: flutter analyze lib/model/expression_data.dart lib/util/mac_address_validator.dart test/model/expression_data_test.dart test/util/mac_address_validator_test.dart

- name: Run Flutter tests with coverage
run: flutter test --coverage test/model/expression_data_test.dart test/util/mac_address_validator_test.dart

- name: Upload app coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: ./app/coverage/lcov.info
flags: app
name: app
fail_ci_if_error: true
use_oidc: true
11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
.DS_Store
.idea/
.idea/

# Go coverage artefacts
server/coverage.out

# Flutter and native build artefacts
app/.dart_tool/
app/.flutter-plugins-dependencies
app/build/
app/coverage/
85 changes: 85 additions & 0 deletions app/test/model/expression_data_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/

import 'package:flutter_test/flutter_test.dart';
import 'package:stack_chan/model/expression_data.dart';

void main() {
group('ExpressionData', () {
test('round-trips JSON for avatar payloads', () {
final expression = ExpressionData(
leftEye: ExpressionItem(x: 1, y: 2, rotation: 3, weight: 4, size: 5),
rightEye: ExpressionItem(
x: 6,
y: 7,
rotation: 8,
weight: 9,
size: 10,
),
mouth: ExpressionItem(
x: 11,
y: 12,
rotation: 13,
weight: 14,
size: 15,
),
);

final decoded = ExpressionData.fromJson(expression.toJson());

expect(decoded.type, 'bleAvatar');
expect(decoded.leftEye.toJson(), expression.leftEye.toJson());
expect(decoded.rightEye.toJson(), expression.rightEye.toJson());
expect(decoded.mouth.toJson(), expression.mouth.toJson());
expect(expression.toString(), contains('"type":"bleAvatar"'));
});

test('copy returns a detached expression item', () {
final original = ExpressionItem(x: 4, y: 5, rotation: 6, weight: 7, size: 8);
final copy = original.copy();

copy.x = 99;

expect(original.x, 4);
expect(copy.toJson(), isNot(original.toJson()));
expect(copy.rotation, original.rotation);
expect(copy.weight, original.weight);
expect(copy.size, original.size);
});
});

group('MotionData', () {
test('prefers angle when serializing zero-rotate motions', () {
final motion = MotionData(
pitchServo: MotionDataItem(angle: 450, speed: 700),
yawServo: MotionDataItem(angle: 120, speed: 500),
);

final decoded = MotionData.fromJson(motion.toJson());

expect(decoded.pitchServo.angle, 450);
expect(decoded.pitchServo.speed, 700);
expect(decoded.yawServo.toJson(), {'angle': 120, 'speed': 500});
});

test('serializes rotate motions without an angle field', () {
final item = MotionDataItem(rotate: 90, speed: 333);

expect(item.toJson(), {'rotate': 90, 'speed': 333});
});
});

group('RgbData', () {
test('uses documented defaults and round-trips JSON', () {
final rgb = RgbData();
final decoded = RgbData.fromJson(rgb.toJson());

expect(decoded.leftRgbColor, '#FFFFFF');
expect(decoded.rightRgbColor, '#FFFFFF');
expect(decoded.leftRgbDuration, 0.0);
expect(decoded.rightRgbDuration, 0.0);
});
});
}
50 changes: 50 additions & 0 deletions app/test/util/mac_address_validator_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/

import 'package:flutter_test/flutter_test.dart';
import 'package:stack_chan/util/mac_address_validator.dart';

void main() {
group('MacAddressValidator.isValidMac', () {
test('accepts supported formats', () {
expect(MacAddressValidator.isValidMac('00:11:22:33:44:55'), isTrue);
expect(MacAddressValidator.isValidMac('aa-bb-cc-dd-ee-ff'), isTrue);
expect(MacAddressValidator.isValidMac('AABBCCDDEEFF'), isTrue);
});

test('rejects malformed values', () {
expect(MacAddressValidator.isValidMac(null), isFalse);
expect(MacAddressValidator.isValidMac(''), isFalse);
expect(MacAddressValidator.isValidMac('GG:11:22:33:44:55'), isFalse);
expect(MacAddressValidator.isValidMac('00112233445'), isFalse);
});
});

group('MacAddressValidator formatting helpers', () {
test('normalize equivalent addresses to a stable form', () {
expect(
MacAddressValidator.formatMac('aa-bb-cc-dd-ee-ff'),
'AA:BB:CC:DD:EE:FF',
);
expect(
MacAddressValidator.formatLowerCaseMac('AABBCCDDEEFF'),
'aa:bb:cc:dd:ee:ff',
);
expect(MacAddressValidator.toPureMac('aa:bb:cc:dd:ee:ff'), 'AABBCCDDEEFF');
expect(MacAddressValidator.normalize('AA-BB-CC-DD-EE-FF'), 'aabbccddeeff');
});

test('compare addresses independent of case and separator', () {
expect(
MacAddressValidator.areEqual('aa:bb:cc:dd:ee:ff', 'AABBCCDDEEFF'),
isTrue,
);
expect(
MacAddressValidator.areEqual('aa:bb:cc:dd:ee:ff', '00:11:22:33:44:55'),
isFalse,
);
});
});
}
34 changes: 0 additions & 34 deletions app/test/widget_test.dart

This file was deleted.

36 changes: 36 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
coverage:
range: 60..85
round: down
precision: 2
status:
project:
default:
target: auto
threshold: 2%
informational: true
patch:
default:
target: 70%
threshold: 0%
informational: true

comment:
layout: "reach,diff,flags,tree"
require_changes: true
behavior: default

flags:
server:
paths:
- server/
app:
paths:
- app/

ignore:
- "app/android/**"
- "app/ios/**"
- "firmware/**"
- "remote/**"
- "server/internal/model/do/**"
- "server/internal/model/entity/**"
Loading
Loading