flutter_live_coverage 1.1.0 copy "flutter_live_coverage: ^1.1.0" to clipboard
flutter_live_coverage: ^1.1.0 copied to clipboard

Flutter runtime code coverage plugin supporting Release mode. Collects line-level coverage data with LCOV output and S3 upload support.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_live_coverage/flutter_live_coverage.dart';

// Note: In actual usage, you need to run the instrumentation command first:
// dart run flutter_live_coverage:coverage_instrument -p ./your_app
// Then import the generated coverage_init.dart file

void main() {
  // Example: Initialize with inline source map (use generated file in real projects)
  _initDemoCoverage();

  runApp(const MyApp());
}

/// Demo coverage initialization
void _initDemoCoverage() {
  // In real projects, you should use CLI-generated initialization code:
  // import 'coverage_init.dart';
  // initCoverage();

  // Here we use an empty source map for demonstration
  const demoSourceMap = '''
  {
    "files": {},
    "lines": {}
  }
  ''';

  CodeCoverageApi.instance.initialize(demoSourceMap);
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Code Coverage Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const CoverageDemoPage(),
    );
  }
}

class CoverageDemoPage extends StatefulWidget {
  const CoverageDemoPage({super.key});

  @override
  State<CoverageDemoPage> createState() => _CoverageDemoPageState();
}

class _CoverageDemoPageState extends State<CoverageDemoPage> {
  final _api = CodeCoverageApi.instance;
  CoverageStats? _stats;
  String _status = 'Ready';
  bool _isLoading = false;
  List<CoverageFileInfo> _localFiles = [];

  @override
  void initState() {
    super.initState();
    _refreshStats();
    _loadLocalFiles();
  }

  void _refreshStats() {
    setState(() {
      _stats = _api.getStats();
    });
  }

  Future<void> _loadLocalFiles() async {
    final files = await _api.listLocalFiles();
    setState(() {
      _localFiles = files;
    });
  }

  Future<void> _saveLocally() async {
    setState(() {
      _isLoading = true;
      _status = 'Saving...';
    });

    try {
      final file = await _api.saveToFile(testName: 'demo-test');
      setState(() {
        _status = 'Saved: ${file.path}';
      });
      await _loadLocalFiles();
    } catch (e) {
      setState(() {
        _status = 'Save failed: $e';
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _configureS3() async {
    // Show configuration dialog
    final result = await showDialog<S3Config>(
      context: context,
      builder: (context) => const S3ConfigDialog(),
    );

    if (result != null) {
      _api.configureS3(result);
      setState(() {
        _status = 'S3 configured: ${result.bucket}';
      });
    }
  }

  Future<void> _uploadToS3() async {
    if (!_api.isS3Configured) {
      setState(() {
        _status = 'Please configure S3 first';
      });
      return;
    }

    setState(() {
      _isLoading = true;
      _status = 'Uploading to S3...';
    });

    try {
      final result = await _api.uploadToS3(testName: 'demo-test');
      setState(() {
        if (result.success) {
          _status = 'Upload success: ${result.url}';
        } else {
          _status = 'Upload failed: ${result.error}';
        }
      });
    } catch (e) {
      setState(() {
        _status = 'Upload error: $e';
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _exportAll() async {
    setState(() {
      _isLoading = true;
      _status = 'Exporting...';
    });

    try {
      final result = await _api.exportCoverage(
        testName: 'demo-export',
        uploadToS3: _api.isS3Configured,
        // Auto-delete local file after successful S3 upload to save space
        deleteLocalOnUploadSuccess: _api.isS3Configured,
      );

      setState(() {
        final parts = <String>[];
        if (result.localFileDeleted) {
          parts.add('Local: deleted after upload');
        } else if (result.localFile != null) {
          parts.add('Local: saved');
        }
        if (result.s3Result != null) {
          parts.add('S3: ${result.s3Result!.success ? "success" : "failed"}');
        }
        parts.add('Coverage: ${result.stats.coveragePercentage.toStringAsFixed(2)}%');
        _status = parts.join(', ');
      });
      await _loadLocalFiles();
    } catch (e) {
      setState(() {
        _status = 'Export failed: $e';
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  void _resetCoverage() {
    _api.reset();
    _refreshStats();
    setState(() {
      _status = 'Coverage data reset';
    });
  }

  Future<void> _cleanupFiles() async {
    final count = await _api.cleanupLocalFiles(keepCount: 5);
    await _loadLocalFiles();
    setState(() {
      _status = 'Cleaned up $count old files';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Code Coverage Demo'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // Statistics card
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Coverage Statistics',
                      style: Theme.of(context).textTheme.titleLarge,
                    ),
                    const SizedBox(height: 8),
                    if (_stats != null) ...[
                      Text('Files: ${_stats!.totalFiles}'),
                      Text('Total lines: ${_stats!.totalLines}'),
                      Text('Executed lines: ${_stats!.executedLines}'),
                      Text(
                        'Coverage: ${_stats!.coveragePercentage.toStringAsFixed(2)}%',
                        style: const TextStyle(
                          fontWeight: FontWeight.bold,
                          fontSize: 18,
                        ),
                      ),
                    ] else
                      const Text('No data'),
                    const SizedBox(height: 8),
                    ElevatedButton.icon(
                      onPressed: _refreshStats,
                      icon: const Icon(Icons.refresh),
                      label: const Text('Refresh'),
                    ),
                  ],
                ),
              ),
            ),

            const SizedBox(height: 16),

            // Action buttons
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Actions',
                      style: Theme.of(context).textTheme.titleLarge,
                    ),
                    const SizedBox(height: 8),
                    Wrap(
                      spacing: 8,
                      runSpacing: 8,
                      children: [
                        ElevatedButton.icon(
                          onPressed: _isLoading ? null : _saveLocally,
                          icon: const Icon(Icons.save),
                          label: const Text('Save Local'),
                        ),
                        ElevatedButton.icon(
                          onPressed: _configureS3,
                          icon: Icon(
                            _api.isS3Configured
                                ? Icons.cloud_done
                                : Icons.cloud_off,
                          ),
                          label: const Text('Configure S3'),
                        ),
                        ElevatedButton.icon(
                          onPressed:
                              _isLoading || !_api.isS3Configured
                                  ? null
                                  : _uploadToS3,
                          icon: const Icon(Icons.cloud_upload),
                          label: const Text('Upload S3'),
                        ),
                        ElevatedButton.icon(
                          onPressed: _isLoading ? null : _exportAll,
                          icon: const Icon(Icons.import_export),
                          label: const Text('Export All'),
                        ),
                        OutlinedButton.icon(
                          onPressed: _resetCoverage,
                          icon: const Icon(Icons.restart_alt),
                          label: const Text('Reset'),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),

            const SizedBox(height: 16),

            // Status display
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Status',
                      style: Theme.of(context).textTheme.titleLarge,
                    ),
                    const SizedBox(height: 8),
                    if (_isLoading)
                      const LinearProgressIndicator()
                    else
                      Text(_status),
                  ],
                ),
              ),
            ),

            const SizedBox(height: 16),

            // Local files list
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Text(
                          'Local Files (${_localFiles.length})',
                          style: Theme.of(context).textTheme.titleLarge,
                        ),
                        IconButton(
                          onPressed: _cleanupFiles,
                          icon: const Icon(Icons.delete_sweep),
                          tooltip: 'Cleanup old files',
                        ),
                      ],
                    ),
                    const SizedBox(height: 8),
                    if (_localFiles.isEmpty)
                      const Text('No saved files')
                    else
                      ...List.generate(
                        _localFiles.length > 5 ? 5 : _localFiles.length,
                        (index) {
                          final file = _localFiles[index];
                          return ListTile(
                            leading: Icon(
                              file.format == CoverageFormat.lcov
                                  ? Icons.description
                                  : Icons.data_object,
                            ),
                            title: Text(
                              file.fileName,
                              overflow: TextOverflow.ellipsis,
                            ),
                            subtitle: Text(file.formattedSize),
                            dense: true,
                          );
                        },
                      ),
                    if (_localFiles.length > 5)
                      Text(
                        '... and ${_localFiles.length - 5} more files',
                        style: Theme.of(context).textTheme.bodySmall,
                      ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

/// S3 configuration dialog
class S3ConfigDialog extends StatefulWidget {
  const S3ConfigDialog({super.key});

  @override
  State<S3ConfigDialog> createState() => _S3ConfigDialogState();
}

class _S3ConfigDialogState extends State<S3ConfigDialog> {
  final _formKey = GlobalKey<FormState>();
  final _bucketController = TextEditingController();
  final _regionController = TextEditingController(text: 'us-west-2');
  final _accessKeyController = TextEditingController();
  final _secretKeyController = TextEditingController();
  final _prefixController = TextEditingController(text: 'coverage');

  @override
  void dispose() {
    _bucketController.dispose();
    _regionController.dispose();
    _accessKeyController.dispose();
    _secretKeyController.dispose();
    _prefixController.dispose();
    super.dispose();
  }

  void _submit() {
    if (_formKey.currentState!.validate()) {
      Navigator.of(context).pop(S3Config(
        bucket: _bucketController.text,
        region: _regionController.text,
        accessKeyId: _accessKeyController.text,
        secretAccessKey: _secretKeyController.text,
        prefix: _prefixController.text.isNotEmpty ? _prefixController.text : null,
      ));
    }
  }

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: const Text('Configure S3'),
      content: Form(
        key: _formKey,
        child: SingleChildScrollView(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextFormField(
                controller: _bucketController,
                decoration: const InputDecoration(labelText: 'Bucket'),
                validator: (v) => v?.isEmpty ?? true ? 'Required' : null,
              ),
              TextFormField(
                controller: _regionController,
                decoration: const InputDecoration(labelText: 'Region'),
                validator: (v) => v?.isEmpty ?? true ? 'Required' : null,
              ),
              TextFormField(
                controller: _accessKeyController,
                decoration: const InputDecoration(labelText: 'Access Key ID'),
                validator: (v) => v?.isEmpty ?? true ? 'Required' : null,
              ),
              TextFormField(
                controller: _secretKeyController,
                decoration: const InputDecoration(labelText: 'Secret Access Key'),
                obscureText: true,
                validator: (v) => v?.isEmpty ?? true ? 'Required' : null,
              ),
              TextFormField(
                controller: _prefixController,
                decoration: const InputDecoration(labelText: 'Prefix (optional)'),
              ),
            ],
          ),
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: const Text('Cancel'),
        ),
        ElevatedButton(
          onPressed: _submit,
          child: const Text('OK'),
        ),
      ],
    );
  }
}
0
likes
150
points
0
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter runtime code coverage plugin supporting Release mode. Collects line-level coverage data with LCOV output and S3 upload support.

Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

analyzer, args, aws_common, aws_signature_v4, flutter, flutter_web_plugins, glob, path, path_provider, plugin_platform_interface, web

More

Packages that depend on flutter_live_coverage

Packages that implement flutter_live_coverage