LCOV - code coverage report
Current view: top level - lib - testing.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 73.1 % 78 57
Test Date: 2026-07-11 13:35:19 Functions: - 0 0

            Line data    Source code
       1              : // Copyright The OpenTelemetry Authors
       2              : // SPDX-License-Identifier: Apache-2.0
       3              : // Copyright 2025, Dartastic.io, All rights reserved.
       4              : 
       5              : /// Test helpers for the Dartastic OpenTelemetry SDK.
       6              : ///
       7              : /// Import this library from `test/` only:
       8              : ///
       9              : /// ```dart
      10              : /// import 'package:dartastic_opentelemetry/testing.dart';
      11              : /// ```
      12              : ///
      13              : /// It is intentionally **not** exported from the main
      14              : /// `package:dartastic_opentelemetry/dartastic_opentelemetry.dart`
      15              : /// barrel so production bundles don't carry the in-memory exporter
      16              : /// classes.
      17              : ///
      18              : /// Provides:
      19              : /// - [InMemorySpanExporter] — collects exported spans into a list with
      20              : ///   query helpers (`findSpanByName`, `findSpansByName`,
      21              : ///   `findSpansStartingWith`, `clear`).
      22              : /// - [InMemoryLogExporter] — same idea for log records.
      23              : /// - [InMemoryMetricExporter] — same idea for metrics; pair with
      24              : ///   [OnDemandMetricReader].
      25              : /// - [OnDemandMetricReader] — a metric reader that never fires on a
      26              : ///   timer; tests drive `collect()` explicitly via
      27              : ///   [TestHarness.collectMetrics].
      28              : /// - [TestHarness] — bundles the three exporters and the reader,
      29              : ///   plus a `clear()` to reset between tests.
      30              : /// - [maybeInitializeOtelForTest] — singleton initializer designed
      31              : ///   for `setUpAll`. Brings up the real OpenTelemetry SDK pointed at
      32              : ///   the in-memory exporters. Safe to call from multiple test files
      33              : ///   in the same process; returns the same harness on subsequent
      34              : ///   calls.
      35              : ///
      36              : /// The shape mirrors the test harness used in the OTel-Dart reference
      37              : /// demo at https://github.com/dartastic/dart-otel-reference-demo
      38              : /// so wrappers, customer apps, and the reference demo all use the same
      39              : /// scaffolding.
      40              : library;
      41              : 
      42              : import 'dart:async';
      43              : 
      44              : import 'dartastic_opentelemetry.dart';
      45              : 
      46              : /// Span exporter that buffers spans in memory for inspection.
      47              : ///
      48              : /// Tests typically:
      49              : ///
      50              : /// ```dart
      51              : /// setUpAll(() async {
      52              : ///   harness = await maybeInitializeOtelForTest();
      53              : ///   spans = harness.spans;
      54              : /// });
      55              : /// setUp(() => harness.clear());
      56              : ///
      57              : /// test('my_op emits a span', () {
      58              : ///   doMyOp();
      59              : ///   expect(spans.findSpanByName('my_op'), isNotNull);
      60              : /// });
      61              : /// ```
      62              : ///
      63              : /// Use [findSpanByName] / [findSpansByName] / [findSpansStartingWith]
      64              : /// instead of indexing into [spans] directly — they make test failures
      65              : /// read more clearly.
      66              : class InMemorySpanExporter implements SpanExporter {
      67              :   /// Creates an exporter.
      68            1 :   InMemorySpanExporter();
      69              : 
      70              :   final List<Span> _spans = <Span>[];
      71              :   bool _isShutdown = false;
      72              : 
      73              :   /// All spans exported since the last [clear].
      74            3 :   List<Span> get spans => List<Span>.unmodifiable(_spans);
      75              : 
      76              :   /// All exported span names, in export order. Handy in `expect`
      77              :   /// assertions when you want to check the trace shape.
      78            6 :   List<String> get spanNames => _spans.map((s) => s.name).toList();
      79              : 
      80              :   /// Clears the recorded spans. Call between tests.
      81            3 :   void clear() => _spans.clear();
      82              : 
      83              :   /// Returns the **most recently** exported span with the given
      84              :   /// [name], or `null` if none match. Most-recent makes per-test
      85              :   /// reads stable when a span name happens to be reused across tests.
      86            1 :   Span? findSpanByName(String name) {
      87            5 :     for (var i = _spans.length - 1; i >= 0; i--) {
      88            6 :       if (_spans[i].name == name) return _spans[i];
      89              :     }
      90              :     return null;
      91              :   }
      92              : 
      93              :   /// Returns every exported span whose name equals [name].
      94            1 :   List<Span> findSpansByName(String name) =>
      95            6 :       _spans.where((s) => s.name == name).toList(growable: false);
      96              : 
      97              :   /// Returns every exported span whose name starts with [prefix].
      98              :   /// Useful for category-level assertions
      99              :   /// (e.g. `findSpansStartingWith('http ')`).
     100            1 :   List<Span> findSpansStartingWith(String prefix) =>
     101            6 :       _spans.where((s) => s.name.startsWith(prefix)).toList(growable: false);
     102              : 
     103            1 :   @override
     104              :   Future<void> export(List<Span> spans) async {
     105            1 :     if (_isShutdown) {
     106            0 :       throw StateError('InMemorySpanExporter is shutdown');
     107              :     }
     108            2 :     _spans.addAll(spans);
     109              :   }
     110              : 
     111            0 :   @override
     112              :   Future<void> forceFlush() async {}
     113              : 
     114            0 :   @override
     115              :   Future<void> shutdown() async {
     116            0 :     _isShutdown = true;
     117              :   }
     118              : }
     119              : 
     120              : /// Log-record exporter that buffers records in memory for inspection.
     121              : class InMemoryLogExporter implements LogRecordExporter {
     122              :   /// Creates an exporter.
     123            1 :   InMemoryLogExporter();
     124              : 
     125              :   final List<LogRecord> _records = <LogRecord>[];
     126              :   bool _isShutdown = false;
     127              : 
     128              :   /// All records exported since the last [clear].
     129            3 :   List<LogRecord> get records => List<LogRecord>.unmodifiable(_records);
     130              : 
     131              :   /// Clears the recorded log records. Call between tests.
     132            3 :   void clear() => _records.clear();
     133              : 
     134              :   /// Returns every record with the given [severity].
     135            1 :   List<LogRecord> findRecordsBySeverity(Severity severity) =>
     136            6 :       _records.where((r) => r.severityNumber == severity).toList();
     137              : 
     138            1 :   @override
     139              :   Future<ExportResult> export(List<LogRecord> r) async {
     140            1 :     if (_isShutdown) return ExportResult.failure;
     141            2 :     _records.addAll(r);
     142              :     return ExportResult.success;
     143              :   }
     144              : 
     145            1 :   @override
     146              :   Future<void> forceFlush() async {}
     147              : 
     148            0 :   @override
     149              :   Future<void> shutdown() async {
     150            0 :     _isShutdown = true;
     151              :   }
     152              : }
     153              : 
     154              : /// Metric exporter that buffers exported metric snapshots in memory.
     155              : ///
     156              : /// Use with [OnDemandMetricReader] so tests drive collection
     157              : /// explicitly — tests rarely want a timer fighting their
     158              : /// `expect` assertions.
     159              : class InMemoryMetricExporter implements MetricExporter {
     160              :   /// Creates an exporter.
     161            1 :   InMemoryMetricExporter();
     162              : 
     163              :   final List<Metric> _metrics = <Metric>[];
     164              :   bool _isShutdown = false;
     165              : 
     166              :   /// All metrics exported since the last [clear].
     167            3 :   List<Metric> get metrics => List<Metric>.unmodifiable(_metrics);
     168              : 
     169              :   /// Clears the recorded metrics. Call between tests.
     170            3 :   void clear() => _metrics.clear();
     171              : 
     172              :   /// Returns the most-recently exported metric named [name], or
     173              :   /// `null` if none match.
     174            1 :   Metric? findMetricByName(String name) {
     175            4 :     for (var i = _metrics.length - 1; i >= 0; i--) {
     176            6 :       if (_metrics[i].name == name) return _metrics[i];
     177              :     }
     178              :     return null;
     179              :   }
     180              : 
     181            1 :   @override
     182              :   Future<bool> export(MetricData data) async {
     183            1 :     if (_isShutdown) return false;
     184            3 :     _metrics.addAll(data.metrics);
     185              :     return true;
     186              :   }
     187              : 
     188            0 :   @override
     189            0 :   Future<bool> forceFlush() async => !_isShutdown;
     190              : 
     191            0 :   @override
     192              :   Future<bool> shutdown() async {
     193            0 :     _isShutdown = true;
     194              :     return true;
     195              :   }
     196              : }
     197              : 
     198              : /// Metric reader for tests. Never fires on a timer; tests drive
     199              : /// `collect()` explicitly via [TestHarness.collectMetrics] so
     200              : /// assertions don't race against a periodic export.
     201              : class OnDemandMetricReader extends MetricReader {
     202              :   /// Creates a reader that forwards collected metrics to [exporter].
     203            1 :   OnDemandMetricReader(this.exporter);
     204              : 
     205              :   /// The exporter that receives collected metrics.
     206              :   final MetricExporter exporter;
     207              :   bool _isShutdown = false;
     208              : 
     209            1 :   @override
     210              :   Future<MetricData> collect() async {
     211            1 :     final mp = meterProvider;
     212            1 :     if (mp == null || _isShutdown) {
     213            0 :       return MetricData.empty();
     214              :     }
     215            1 :     final metrics = await mp.collectAllMetrics();
     216            2 :     return MetricData(resource: mp.resource, metrics: metrics);
     217              :   }
     218              : 
     219            0 :   @override
     220              :   Future<bool> forceFlush() async {
     221            0 :     if (_isShutdown) return false;
     222            0 :     final data = await collect();
     223            0 :     if (data.metrics.isNotEmpty) {
     224            0 :       await exporter.export(data);
     225              :     }
     226            0 :     return await exporter.forceFlush();
     227              :   }
     228              : 
     229            0 :   @override
     230              :   Future<bool> shutdown() async {
     231            0 :     if (_isShutdown) return true;
     232            0 :     _isShutdown = true;
     233            0 :     return await exporter.shutdown();
     234              :   }
     235              : }
     236              : 
     237              : /// Bundles the three in-memory exporters and the on-demand metric
     238              : /// reader so tests have one handle to `clear()` between cases.
     239              : ///
     240              : /// Construct via [maybeInitializeOtelForTest].
     241              : class TestHarness {
     242              :   /// Creates a harness. Prefer [maybeInitializeOtelForTest] over
     243              :   /// calling this directly.
     244            1 :   TestHarness({
     245              :     required this.spans,
     246              :     required this.logs,
     247              :     required this.metrics,
     248              :     required this.metricReader,
     249              :   });
     250              : 
     251              :   /// The span exporter that received every emitted span.
     252              :   final InMemorySpanExporter spans;
     253              : 
     254              :   /// The log exporter that received every emitted log record.
     255              :   final InMemoryLogExporter logs;
     256              : 
     257              :   /// The metric exporter that received every emitted metric snapshot.
     258              :   /// New snapshots arrive only when you call [collectMetrics].
     259              :   final InMemoryMetricExporter metrics;
     260              : 
     261              :   /// The reader driving metric collection.
     262              :   final MetricReader metricReader;
     263              : 
     264              :   /// Pumps the meter provider and forwards collected metrics to the
     265              :   /// in-memory exporter. Call this in a test after recording metrics
     266              :   /// but before asserting on [metrics].
     267            1 :   Future<void> collectMetrics() async {
     268            2 :     final data = await metricReader.collect();
     269            2 :     await metrics.export(data);
     270              :   }
     271              : 
     272              :   /// Forces the SDK's logger pipeline to flush. Tests that emit logs
     273              :   /// synchronously usually don't need this, but it's safe to call.
     274            1 :   Future<void> flushLogs() async {
     275            2 :     await OTel.loggerProvider().forceFlush();
     276              :   }
     277              : 
     278              :   /// Resets all three exporters' buffers. Call from `setUp()`.
     279            1 :   void clear() {
     280            2 :     spans.clear();
     281            2 :     logs.clear();
     282            2 :     metrics.clear();
     283              :   }
     284              : }
     285              : 
     286              : TestHarness? _shared;
     287              : 
     288              : /// Initializes the OpenTelemetry SDK once per test process with
     289              : /// in-memory exporters for spans / logs / metrics, and returns the
     290              : /// same [TestHarness] on subsequent calls.
     291              : ///
     292              : /// Designed for `setUpAll`:
     293              : ///
     294              : /// ```dart
     295              : /// late TestHarness harness;
     296              : /// late InMemorySpanExporter spans;
     297              : ///
     298              : /// setUpAll(() async {
     299              : ///   harness = await maybeInitializeOtelForTest(
     300              : ///     serviceName: 'my_wrapper-test',
     301              : ///   );
     302              : ///   spans = harness.spans;
     303              : /// });
     304              : ///
     305              : /// setUp(() => harness.clear());
     306              : /// ```
     307              : ///
     308              : /// Idempotent — calling it from multiple test files in the same
     309              : /// process is fine; the SDK is initialized exactly once and every
     310              : /// caller gets the same exporters.
     311              : ///
     312              : /// - [serviceName] becomes `service.name` on emitted telemetry.
     313              : /// - [endpoint] is a dummy — no exporter actually hits the network.
     314              : ///   Override only if your code-under-test reads it.
     315            1 : Future<TestHarness> maybeInitializeOtelForTest({
     316              :   String serviceName = 'otel-test',
     317              :   String endpoint = OTel.defaultEndpoint,
     318              : }) async {
     319              :   if (_shared != null) return _shared!;
     320            1 :   final spanExporter = InMemorySpanExporter();
     321            1 :   final logExporter = InMemoryLogExporter();
     322            1 :   final metricExporter = InMemoryMetricExporter();
     323            1 :   final reader = OnDemandMetricReader(metricExporter);
     324            1 :   await OTel.initialize(
     325              :     endpoint: endpoint,
     326              :     serviceName: serviceName,
     327              :     serviceVersion: '0.0.0-test',
     328            1 :     spanProcessor: SimpleSpanProcessor(spanExporter),
     329            1 :     logRecordProcessor: SimpleLogRecordProcessor(logExporter),
     330              :     metricReader: reader,
     331              :     detectPlatformResources: false,
     332              :   );
     333            1 :   return _shared = TestHarness(
     334              :     spans: spanExporter,
     335              :     logs: logExporter,
     336              :     metrics: metricExporter,
     337              :     metricReader: reader,
     338              :   );
     339              : }
        

Generated by: LCOV version 2.0-1