sate_ai 0.7.1
sate_ai: ^0.7.1 copied to clipboard
A fault injection framework for testing on-device AI models in Flutter. Simulate memory pressure, malformed inputs, and degradation to catch failures before deployment.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sate_ai/sate_ai.dart';
void main() => runApp(const SateAIApp());
enum AdapterType { mock, onnx, tflite }
class SateAIApp extends StatelessWidget {
const SateAIApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SATE AI Stress Test Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6C63FF),
brightness: Brightness.dark,
),
useMaterial3: true,
),
home: const StressDashboard(),
);
}
}
class StressDashboard extends StatefulWidget {
const StressDashboard({super.key});
@override
State<StressDashboard> createState() => _StressDashboardState();
}
class _StressDashboardState extends State<StressDashboard>
with TickerProviderStateMixin {
AdapterType _selectedAdapter = AdapterType.mock;
StressReport? _report;
bool _running = false;
String _status = 'Ready';
final List<String> _log = [];
late AnimationController _pulseCtrl;
// Selected injectors
bool _useMemoryPressure = true;
bool _useMalformedInput = true;
bool _useThermalThrottle = true;
bool _useQuantizationDrift = true;
bool _useConfidenceValidation = true;
@override
void initState() {
super.initState();
_pulseCtrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
)..repeat(reverse: true);
}
@override
void dispose() {
_pulseCtrl.dispose();
super.dispose();
}
Future<AIModelAdapter> _createAdapter() async {
switch (_selectedAdapter) {
case AdapterType.mock:
return MockAdapter(
modelId: 'mock-llm-v1',
inferenceDelay: const Duration(milliseconds: 150),
);
case AdapterType.onnx:
_appendLog('๐ฆ Initializing OnnxAdapter (assets/models/mobilenet.onnx)...');
try {
final bytes = await rootBundle.load('assets/models/mobilenet.onnx');
return OnnxAdapter(
modelId: 'onnx-mobilenet-v2',
modelBytes: bytes.buffer.asUint8List(),
);
} catch (e) {
_appendLog('โ ๏ธ ONNX init fallback to Mock: $e');
return MockAdapter(modelId: 'onnx-fallback-mock');
}
case AdapterType.tflite:
_appendLog('๐ฆ Initializing TFLiteAdapter (assets/models/mobilenet.tflite)...');
try {
return await TFLiteAdapter.fromAsset(
'assets/models/mobilenet.tflite',
modelId: 'tflite-mobilenet-v1',
);
} catch (e) {
_appendLog('โ ๏ธ TFLite init fallback to Mock: $e');
return MockAdapter(modelId: 'tflite-fallback-mock');
}
}
}
Future<void> _runStressTest() async {
setState(() {
_running = true;
_report = null;
_log.clear();
_status = 'Initialising selected model adapter...';
});
_appendLog('๐ค Selected Adapter: ${_selectedAdapter.name.toUpperCase()}');
final model = await _createAdapter();
_appendLog('โ
Model initialized: ${model.modelId}');
final List<FaultInjector> injectors = [];
if (_useMemoryPressure) {
injectors.add(MemoryPressureInjector(model: model, limitMb: 120));
_appendLog('โ๏ธ Configured MemoryPressureInjector (120 MB)');
}
if (_useMalformedInput) {
injectors.add(const MalformedInputInjector());
_appendLog('โ๏ธ Configured MalformedInputInjector');
}
if (_useThermalThrottle) {
injectors.add(
ThermalThrottleInjector(
model: model,
temperatureStep: 15,
maxTemperature: 80,
),
);
_appendLog('โ๏ธ Configured ThermalThrottleInjector (Max 80ยฐC)');
}
if (_useQuantizationDrift) {
injectors.add(
QuantizationDriftInjector(
model: model,
driftFactor: 0.1,
degradationThreshold: 0.3,
),
);
_appendLog('โ๏ธ Configured QuantizationDriftInjector');
}
if (_useConfidenceValidation) {
injectors.add(ConfidenceThresholdInjector(model: model, threshold: 0.6));
_appendLog('โ๏ธ Configured ConfidenceThresholdInjector (0.6)');
}
if (injectors.isEmpty) {
_appendLog('โ ๏ธ No injectors selected. Adding default MalformedInputInjector.');
injectors.add(const MalformedInputInjector());
}
setState(() => _status = 'Running SateAI.stress() runner...');
_appendLog('๐งช Starting stress runner evaluation (${injectors.length} injectors)...');
final report = await SateAI.stress(
model: model,
injectors: injectors,
timeout: const Duration(seconds: 30),
);
for (final result in report.results) {
final icon = result.passed ? 'โ
' : 'โ';
final timeMs = result.inferenceTime?.inMilliseconds ?? 0;
_appendLog(
'$icon ${result.injectorType.displayName}: ${timeMs}ms '
'${result.memoryUsageMB != null ? "(${result.memoryUsageMB}MB)" : ""}',
);
}
setState(() {
_running = false;
_report = report;
_status = report.passed
? 'โ
Stress Suite Passed (${report.passCount}/${report.totalTests})'
: 'โ Failures Detected (${report.failureCount} failed)';
});
}
void _appendLog(String line) => setState(() => _log.add(line));
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: const Color(0xFF0F0F1A),
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6C63FF), Color(0xFF48CAE4)],
),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.science, color: Colors.white, size: 18),
),
const SizedBox(width: 10),
const Text(
'SATE AI Stress Test Demo',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
fontSize: 16,
),
),
],
),
),
body: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Row 1: Compact Horizontal Adapter Selector
_buildAdapterSelectorRow(),
const SizedBox(height: 8),
// Row 2: Horizontal Sliding Injectors Row (Left-to-Right Scrollable)
_buildInjectorScrollRow(),
const SizedBox(height: 8),
// Compact Status Banner + Run Button Row
Row(
children: [
Expanded(child: _buildStatusCard(cs)),
const SizedBox(width: 8),
_buildRunButton(cs),
],
),
const SizedBox(height: 8),
if (_report != null) ...[
_buildSummaryCard(_report!),
const SizedBox(height: 8),
],
// Maximized Execution Log Screen (Fills remaining height)
Expanded(child: _buildLogCard()),
],
),
),
);
}
// Row layout for Select Model Adapter
Widget _buildAdapterSelectorRow() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF1A1A2E),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white12),
),
child: Row(
children: [
const Text(
'ADAPTER:',
style: TextStyle(
color: Colors.white70,
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 0.8,
),
),
const SizedBox(width: 8),
Expanded(
child: Row(
children: AdapterType.values.map((type) {
final isSelected = _selectedAdapter == type;
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 3),
child: ChoiceChip(
padding: EdgeInsets.zero,
labelPadding: const EdgeInsets.symmetric(horizontal: 4),
visualDensity: VisualDensity.compact,
label: Center(
child: Text(
type.name.toUpperCase(),
style: TextStyle(
color: isSelected ? Colors.white : Colors.white60,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 11,
),
),
),
selected: isSelected,
selectedColor: const Color(0xFF6C63FF),
backgroundColor: const Color(0xFF0F0F1A),
onSelected: (val) {
if (val) setState(() => _selectedAdapter = type);
},
),
),
);
}).toList(),
),
),
],
),
);
}
// Horizontal Left-to-Right Sliding Injectors Row
Widget _buildInjectorScrollRow() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF1A1A2E),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'INJECTORS (SWIPE LEFT/RIGHT):',
style: TextStyle(
color: Colors.white70,
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 0.8,
),
),
const SizedBox(height: 4),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
child: Row(
children: [
_buildInjectorChip('Memory Pressure', _useMemoryPressure, (v) => setState(() => _useMemoryPressure = v)),
const SizedBox(width: 6),
_buildInjectorChip('Malformed Input', _useMalformedInput, (v) => setState(() => _useMalformedInput = v)),
const SizedBox(width: 6),
_buildInjectorChip('Thermal Throttle', _useThermalThrottle, (v) => setState(() => _useThermalThrottle = v)),
const SizedBox(width: 6),
_buildInjectorChip('Quantization Drift', _useQuantizationDrift, (v) => setState(() => _useQuantizationDrift = v)),
const SizedBox(width: 6),
_buildInjectorChip('Confidence Validator', _useConfidenceValidation, (v) => setState(() => _useConfidenceValidation = v)),
],
),
),
],
),
);
}
Widget _buildInjectorChip(String label, bool selected, ValueChanged<bool> onSelected) {
return FilterChip(
visualDensity: VisualDensity.compact,
labelPadding: const EdgeInsets.symmetric(horizontal: 4),
label: Text(label, style: const TextStyle(fontSize: 11)),
selected: selected,
selectedColor: const Color(0xFF48CAE4).withAlpha(180),
checkmarkColor: Colors.white,
onSelected: onSelected,
);
}
Widget _buildStatusCard(ColorScheme cs) {
final color = _running
? const Color(0xFF48CAE4)
: (_report?.passed ?? true)
? const Color(0xFF06D6A0)
: const Color(0xFFEF476F);
return AnimatedBuilder(
animation: _pulseCtrl,
builder: (context, child) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF1A1A2E),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: _running
? color.withAlpha(((_pulseCtrl.value * 180 + 75).round()))
: color.withAlpha(80),
width: 1.5,
),
),
child: Row(
children: [
_running
? SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: color,
),
)
: Icon(
_report == null
? Icons.pending_outlined
: (_report!.passed ? Icons.check_circle : Icons.error),
color: color,
size: 18,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_status,
style: TextStyle(
color: color,
fontWeight: FontWeight.w600,
fontSize: 12,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
},
);
}
Widget _buildRunButton(ColorScheme cs) {
return GestureDetector(
onTap: _running ? null : _runStressTest,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
gradient: _running
? const LinearGradient(
colors: [Color(0xFF3A3A5C), Color(0xFF2A2A4A)],
)
: const LinearGradient(
colors: [Color(0xFF6C63FF), Color(0xFF48CAE4)],
),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_running ? Icons.hourglass_top : Icons.play_arrow_rounded,
color: Colors.white,
size: 18,
),
const SizedBox(width: 6),
Text(
_running ? 'Running...' : 'Run Suite',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
fontSize: 13,
),
),
],
),
),
);
}
Widget _buildSummaryCard(StressReport report) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF1A1A2E),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_StatChip(label: 'Total', value: '${report.totalTests}', color: const Color(0xFF48CAE4)),
_StatChip(label: 'Passed', value: '${report.passCount}', color: const Color(0xFF06D6A0)),
_StatChip(label: 'Failed', value: '${report.failureCount}', color: const Color(0xFFEF476F)),
_StatChip(label: 'Duration', value: '${report.totalDuration.inMilliseconds}ms', color: const Color(0xFFFFD166)),
],
),
);
}
// Maximized Execution Log Card
Widget _buildLogCard() {
return Container(
decoration: BoxDecoration(
color: const Color(0xFF0D0D1A),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.fromLTRB(12, 8, 12, 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'EXECUTION LOG (MAXIMIZED SCREEN)',
style: TextStyle(
color: Colors.white54,
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
),
),
Icon(Icons.terminal, color: Colors.white38, size: 14),
],
),
),
const Divider(color: Colors.white10, height: 1),
Expanded(
child: _log.isEmpty
? const Center(
child: Text(
'Select Model Adapter & Injectors, then tap "Run Suite"',
style: TextStyle(color: Colors.white24, fontSize: 12),
),
)
: ListView.builder(
padding: const EdgeInsets.all(10),
itemCount: _log.length,
itemBuilder: (ctx, i) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
_log[i],
style: const TextStyle(
color: Color(0xFFB0B8D8),
fontFamily: 'monospace',
fontSize: 12,
),
),
),
),
),
],
),
);
}
}
class _StatChip extends StatelessWidget {
const _StatChip({
required this.label,
required this.value,
required this.color,
});
final String label;
final String value;
final Color color;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
value,
style: TextStyle(
color: color,
fontSize: 15,
fontWeight: FontWeight.w800,
),
),
Text(
label,
style: const TextStyle(
color: Colors.white38,
fontSize: 9,
fontWeight: FontWeight.w600,
),
),
],
);
}
}