LCOV - code coverage report
Current view: top level - src - testainers_base.dart (source / functions) Coverage Total Hit
Test: testainers Lines: 85.3 % 102 87
Test Date: 2026-05-24 17:06:20 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:convert';
       2              : import 'dart:io';
       3              : 
       4              : import 'package:testainers/src/testainers_config.dart';
       5              : import 'package:testainers/src/testainers_exception.dart';
       6              : import 'package:testainers/src/testainers_network.dart';
       7              : import 'package:testainers/src/testainers_utils.dart';
       8              : 
       9              : class Testainers {
      10              :   final String image;
      11              :   final String tag;
      12              :   final Map<int, int> ports;
      13              :   final Map<String, String> env;
      14              :   final bool detached;
      15              :   final bool remove;
      16              :   final List<String> links;
      17              :   final List<TestainersNetwork> networks;
      18              :   final String healthCmd;
      19              :   final int healthInterval;
      20              :   final int healthRetries;
      21              :   final int healthTimeout;
      22              :   final int healthStartPeriod;
      23              :   final bool noHealthcheck;
      24              :   final int stopTime;
      25              :   final List<String> command;
      26              :   final TestainersConfig config;
      27              :   final String name;
      28              :   String? id;
      29              : 
      30            9 :   Testainers({
      31              :     required this.image,
      32              :     required this.tag,
      33              :     required this.ports,
      34              :     required this.env,
      35              :     required this.detached,
      36              :     required this.remove,
      37              :     this.links = const <String>[],
      38              :     this.networks = const <TestainersNetwork>[],
      39              :     this.healthCmd = '',
      40              :     this.healthInterval = 0,
      41              :     this.healthRetries = 0,
      42              :     this.healthTimeout = 0,
      43              :     this.healthStartPeriod = 0,
      44              :     this.noHealthcheck = false,
      45              :     this.stopTime = 0,
      46              :     this.command = const <String>[],
      47              :     this.config = const TestainersConfig(),
      48              :     String? name,
      49            9 :   }) : name = name ?? TestainersUtils.generateName();
      50              : 
      51            2 :   Future<Map<int, int>> portsFilter(Map<int, int> ports) async => ports;
      52              : 
      53            6 :   Future<Map<String, String>> envFilter(Map<String, String> env) async => env;
      54              : 
      55            9 :   Future<List<String>> linksFilter(List<String> links) async => links;
      56              : 
      57            9 :   Future<List<TestainersNetwork>> networksFilter(
      58              :     List<TestainersNetwork> networks,
      59              :   ) async =>
      60              :       networks;
      61              : 
      62            9 :   Future<List<String>> commandFilter(List<String> command) async => command;
      63              : 
      64            9 :   Future<String> start({Duration? bootSleep}) async {
      65           27 :     if (config.runner.isEmpty) {
      66            1 :       throw TestainersException('Runner not defined.');
      67              :     }
      68              : 
      69           27 :     if (config.defaultUsername.isEmpty) {
      70            1 :       throw TestainersException('Default username not defined.');
      71              :     }
      72              : 
      73           27 :     if (config.defaultPassword.isEmpty) {
      74            1 :       throw TestainersException('Default password not defined.');
      75              :     }
      76              : 
      77           18 :     if (image.isEmpty) {
      78            1 :       throw TestainersException('Image not defined.');
      79              :     }
      80              : 
      81           18 :     if (tag.isEmpty) {
      82            1 :       throw TestainersException('Tag not defined.');
      83              :     }
      84              : 
      85           18 :     await config.runnerVersion();
      86              : 
      87            9 :     final List<String> arguments = <String>[
      88              :       'run',
      89           18 :       if (detached) '-d',
      90           18 :       if (remove) '--rm',
      91            9 :       '--name',
      92            9 :       name,
      93              :     ];
      94              : 
      95           18 :     final Map<int, int> effectivePorts = await portsFilter(ports);
      96              : 
      97           17 :     for (final MapEntry<int, int> entry in effectivePorts.entries) {
      98              :       /// Zero is a docker random port.
      99           16 :       if (entry.key <= 0 ||
     100           16 :           entry.key > 65535 ||
     101           16 :           entry.value <= 0 ||
     102           16 :           entry.value > 65535) {
     103            1 :         throw TestainersException(
     104            3 :           'Invalid port ${entry.key}:${entry.value}.',
     105              :         );
     106              :       }
     107              : 
     108              :       arguments
     109            8 :         ..add('-p')
     110           32 :         ..add('${entry.key}:${entry.value}');
     111              :     }
     112              : 
     113           18 :     final Map<String, String> effectiveEnv = await envFilter(env);
     114              : 
     115           12 :     for (final MapEntry<String, String> entry in effectiveEnv.entries) {
     116              :       arguments
     117            3 :         ..add('-e')
     118           12 :         ..add('${entry.key}=${entry.value}');
     119              :     }
     120              : 
     121           27 :     for (final String link in await linksFilter(links)) {
     122            0 :       if (link.isNotEmpty) {
     123              :         arguments
     124            0 :           ..add('--link')
     125            0 :           ..add(link);
     126              :       }
     127              :     }
     128              : 
     129           27 :     for (final TestainersNetwork network in await networksFilter(networks)) {
     130              :       arguments
     131            0 :         ..add('--network')
     132            0 :         ..add(network.name);
     133              :     }
     134              : 
     135           22 :     if (healthCmd.isNotEmpty && !noHealthcheck) {
     136            9 :       arguments.add('--health-cmd=$healthCmd');
     137              : 
     138            6 :       if (healthInterval > 0) {
     139            9 :         arguments.add('--health-interval=${healthInterval}s');
     140              :       }
     141              : 
     142            6 :       if (healthRetries > 0) {
     143            9 :         arguments.add('--health-retries=$healthRetries');
     144              :       }
     145              : 
     146            6 :       if (healthTimeout > 0) {
     147            9 :         arguments.add('--health-timeout=${healthTimeout}s');
     148              :       }
     149              : 
     150            6 :       if (healthStartPeriod > 0) {
     151            9 :         arguments.add('--health-start-period=${healthStartPeriod}s');
     152              :       }
     153              :     }
     154              : 
     155            9 :     if (noHealthcheck) {
     156            1 :       arguments.add('--no-healthcheck');
     157              :     }
     158              : 
     159           36 :     arguments.add('$image:$tag');
     160              : 
     161           28 :     for (final String command in await commandFilter(this.command)) {
     162            1 :       arguments.add(command);
     163              :     }
     164              : 
     165           27 :     id = await config.exec(
     166              :       arguments: arguments,
     167           36 :       exceptionExec: 'Container $name with image $image:$tag not started.',
     168              :     );
     169              : 
     170           22 :     if (healthCmd.isEmpty || noHealthcheck) {
     171           16 :       await Future<void>.delayed(bootSleep ?? config.bootSleep);
     172              :     } else {
     173              :       late Map<dynamic, dynamic> inspectMap;
     174              : 
     175              :       String healthStatus = 'starting';
     176              : 
     177            3 :       while (healthStatus == 'starting') {
     178            3 :         await Future<void>.delayed(const Duration(seconds: 1));
     179              : 
     180            6 :         final String inspectString = await config.exec(
     181            3 :           arguments: <String>[
     182              :             'inspect',
     183              :             "--format='{{json .State.Health}}'",
     184            3 :             name,
     185              :           ],
     186           12 :           exceptionExec: 'Container $name with image $image:$tag '
     187              :               'could not be inspected.',
     188              :         );
     189              : 
     190              :         inspectMap =
     191           12 :             jsonDecode(inspectString.substring(1, inspectString.length - 1));
     192              : 
     193            6 :         if (config.debug) {
     194              :           // ignore: avoid_print
     195            0 :           print('Inspect: $inspectMap');
     196              :         }
     197              : 
     198            3 :         healthStatus = inspectMap['Status'];
     199              :       }
     200              : 
     201            3 :       if (healthStatus != 'healthy') {
     202            0 :         throw TestainersException(
     203            0 :           'Container $name with image $image:$tag is $healthStatus.',
     204              :           reason: inspectMap,
     205              :         );
     206              :       }
     207              :     }
     208              : 
     209            9 :     return id!;
     210              :   }
     211              : 
     212              :   /// Executes a command inside a running container.
     213              :   ///
     214              :   /// The container must be started (via [start]) before calling this method.
     215              :   /// Uses `docker exec` (or the configured runner) to run the given [command]
     216              :   /// inside the container identified by [name].
     217              :   ///
     218              :   /// Parameters:
     219              :   /// - [command] — the command and its arguments to execute inside the
     220              :   ///   container (e.g., `['echo', 'hello']` or `['psql', '-c', 'SELECT 1']`).
     221              :   /// - [interactive] — if `true`, allocates a pseudo-TTY (`-i -t` flags).
     222              :   ///   Defaults to `false`, which runs without TTY allocation
     223              :   ///   (suitable for non-interactive / scripted usage).
     224              :   ///
     225              :   /// Returns the trimmed stdout output of the executed command.
     226              :   ///
     227              :   /// Throws [TestainersException] if the container has not been started
     228              :   /// (i.e., [id] is `null`) or if the command exits with a non-zero code.
     229              :   ///
     230              :   /// Example:
     231              :   /// ```dart
     232              :   /// final container = Testainers(
     233              :   ///   image: 'alpine', tag: 'latest',
     234              :   ///   detached: true, remove: true,
     235              :   ///   env: {}, ports: {},
     236              :   /// );
     237              :   /// await container.start();
     238              :   ///
     239              :   /// final output = await container.exec(command: ['echo', 'hello']);
     240              :   /// print(output); // 'hello'
     241              :   ///
     242              :   /// await container.stop();
     243              :   /// ```
     244            1 :   Future<String> exec({
     245              :     required List<String> command,
     246              :     bool interactive = false,
     247              :   }) async {
     248            1 :     if (id == null) {
     249            3 :       throw TestainersException('Container $name not started.');
     250              :     }
     251              : 
     252            1 :     final List<String> arguments = <String>[
     253              :       'exec',
     254            0 :       if (interactive) ...<String>['-i', '-t'],
     255            1 :       name,
     256            1 :       ...command,
     257              :     ];
     258              : 
     259            2 :     return config.exec(
     260              :       arguments: arguments,
     261            2 :       exceptionExec: 'Exec failed in container $name.',
     262              :     );
     263              :   }
     264              : 
     265              :   /// Stops the container.
     266              :   ///
     267              :   /// Sends a stop signal to the running container identified by [name].
     268              :   /// If [stopTime] is greater than zero, it is passed as the `--timeout`
     269              :   /// argument to the stop command.
     270              :   ///
     271              :   /// Throws [TestainersException] if the container could not be stopped
     272              :   /// or if the stop output does not match the expected container name.
     273            9 :   Future<void> stop() async {
     274            9 :     final List<String> arguments = <String>[
     275              :       'stop',
     276           21 :       if (stopTime > 0) ...<String>['--timeout', '$stopTime'],
     277            9 :       name,
     278              :     ];
     279              : 
     280            9 :     final ProcessResult result = await Process.run(
     281           18 :       config.runner,
     282              :       arguments,
     283           27 :     ).timeout(config.timeout);
     284              : 
     285           14 :     if (result.exitCode != 0) {
     286            0 :       throw TestainersException(
     287            0 :         'Container $name with image $image:$tag not stopped.',
     288            0 :         reason: result.stderr,
     289              :       );
     290              :     }
     291              : 
     292           35 :     if (result.stdout.toString().trim() != name) {
     293            0 :       throw TestainersException(
     294            0 :         'Check if the container $name has stopped.',
     295            0 :         reason: result.stdout,
     296              :       );
     297              :     }
     298              :   }
     299              : }
        

Generated by: LCOV version 2.0-1