/// <summary> /// Used to start the fixture within a <see cref="ComposedFixture"/>. /// </summary> /// <param name="settings">Optional Cadence settings.</param> /// <param name="image">Optionally specifies the Cadence container image (defaults to <b>nkubeio/cadence-dev:latest</b>).</param> /// <param name="name">Optionally specifies the Cadence container name (defaults to <c>cb-test</c>).</param> /// <param name="env">Optional environment variables to be passed to the Cadence container, formatted as <b>NAME=VALUE</b> or just <b>NAME</b>.</param> /// <param name="defaultDomain">Optionally specifies the default domain for the fixture's client. This defaults to <b>test-domain</b>.</param> /// <param name="logLevel">Specifies the Cadence log level. This defaults to <see cref="LogLevel.None"/>.</param> /// <param name="keepConnection"> /// Optionally specifies that a new Cadence connection <b>should not</b> be established for each /// unit test case. The same connection will be reused which will save about a second per test. /// </param> /// <param name="keepOpen"> /// Optionally indicates that the container should continue to run after the fixture is disposed. /// </param> /// <param name="hostInterface"> /// Optionally specifies the host interface where the container public ports will be /// published. This defaults to <see cref="ContainerFixture.DefaultHostInterface"/> /// but may be customized. This needs to be an IPv4 address. /// </param> /// <param name="noClient"> /// Optionally disables establishing a client connection when <c>true</c> /// is passed. The <see cref="Client"/> and <see cref="HttpClient"/> properties /// will be set to <c>null</c> in this case. /// </param> /// <param name="noReset"> /// Optionally prevents the fixture from calling <see cref="CadenceClient.Reset()"/> to /// put the Cadence client library into its initial state before the fixture starts as well /// as when the fixture itself is reset. /// </param> /// <param name="emulateProxy"> /// <b>INTERNAL USE ONLY:</b> Optionally starts a partially functional integrated /// <b>cadence-proxy</b> for low-level testing. Most users should never enable this /// because it's probably not going to do what you expect. /// </param> /// <remarks> /// <note> /// A fresh Cadence client <see cref="Client"/> will be established every time this /// fixture is started, regardless of whether the fixture has already been started. This /// ensures that each unit test will start with a client in the default state. /// </note> /// </remarks> public void StartAsComposed( CadenceSettings settings = null, string image = "nkubeio/cadence-dev:latest", string name = "cadence-dev", string[] env = null, string defaultDomain = DefaultDomain, LogLevel logLevel = LogLevel.None, bool keepConnection = false, bool keepOpen = false, string hostInterface = null, bool noClient = false, bool noReset = false, bool emulateProxy = false) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(image), nameof(image)); base.CheckWithinAction(); if (!IsRunning) { if (string.IsNullOrEmpty(hostInterface)) { hostInterface = ContainerFixture.DefaultHostInterface; } else { Covenant.Requires <ArgumentException>(IPAddress.TryParse(hostInterface, out var address) && address.AddressFamily == AddressFamily.InterNetwork, nameof(hostInterface), $"[{hostInterface}] is not a valid IPv4 address."); } // Reset CadenceClient to its initial state. this.noReset = noReset; if (!noReset) { CadenceClient.Reset(); } // Start the Cadence container. base.StartAsComposed(name, image, new string[] { "--detach", "-p", $"{GetHostInterface(hostInterface)}:7933-7939:7933-7939", "-p", $"{GetHostInterface(hostInterface)}:8088:8088" }, env: env, keepOpen: keepOpen); Thread.Sleep(warmupDelay); // Initialize the settings. settings = settings ?? new CadenceSettings() { CreateDomain = true, DefaultDomain = defaultDomain, LogLevel = logLevel }; settings.Servers.Clear(); settings.Servers.Add($"http://{GetHostInterface(hostInterface, forConnection: true)}:{NetworkPorts.Cadence}"); this.settings = settings; this.keepConnection = keepConnection; if (!noClient) { // Establish the Cadence connection. Client = CadenceClient.ConnectAsync(settings).Result; HttpClient = new HttpClient() { BaseAddress = Client.ListenUri }; } } }
/// <summary> /// Used to start the fixture within a <see cref="ComposedFixture"/>. /// </summary> /// <param name="settings">Optional Cadence settings.</param> /// <param name="name">Optionally specifies the Cadence container name (defaults to <c>cadence-dev</c>).</param> /// <param name="composeFile"> /// <para> /// Optionally specifies the Temporal Docker compose file text. This defaults to /// <see cref="DefaultComposeFile"/> which configures Temporal server to start with /// a new Cassandra database instance listening on port <b>9042</b> as well as the /// Temporal web UI running on port <b>8088</b>. Temporal server is listening on /// its standard gRPC port <b>7233</b>. /// </para> /// <para> /// You may specify your own Docker compose text file to customize this by configuring /// a different backend database, etc. /// </para> /// </param> /// <param name="defaultDomain">Optionally specifies the default domain for the fixture's client. This defaults to <b>test-domain</b>.</param> /// <param name="logLevel">Specifies the Cadence log level. This defaults to <see cref="Neon.Diagnostics.LogLevel.None"/>.</param> /// <param name="reconnect"> /// Optionally specifies that a new Cadence connection <b>should</b> be established for each /// unit test case. By default, the same connection will be reused which will save about a /// second per test case. /// </param> /// <param name="keepRunning"> /// Optionally indicates that the container should remain running after the fixture is disposed. /// This is handy for using the Temporal web UI for port mortems after tests have completed. /// </param> /// <param name="noClient"> /// Optionally disables establishing a client connection when <c>true</c> /// is passed. The <see cref="Client"/> and <see cref="HttpClient"/> properties /// will be set to <c>null</c> in this case. /// </param> /// <param name="noReset"> /// Optionally prevents the fixture from calling <see cref="CadenceClient.Reset()"/> to /// put the Cadence client library into its initial state before the fixture starts as well /// as when the fixture itself is reset. /// </param> /// <remarks> /// <note> /// A fresh Cadence client <see cref="Client"/> will be established every time this /// fixture is started, regardless of whether the fixture has already been started. This /// ensures that each unit test will start with a client in the default state. /// </note> /// </remarks> public void StartAsComposed( CadenceSettings settings = null, string name = "cadence-dev", string composeFile = DefaultComposeFile, string defaultDomain = DefaultDomain, LogLevel logLevel = LogLevel.None, bool reconnect = false, bool keepRunning = false, bool noClient = false, bool noReset = false) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(composeFile), nameof(composeFile)); base.CheckWithinAction(); if (!IsRunning) { // $hack(jefflill): // // The [temporal-dev] Docker test stack may be running from a previous test run. // We need to stop this to avoid network port conflicts. We're just going to // force the removal of the stack's Docker containers. // // This is somewhat fragile because it hardcodes the container names and won't // remove any other stack assets like networks. NeonHelper.ExecuteCapture(NeonHelper.DockerCli, new object[] { "rm", "--force", new string[] { "temporal-dev_cassandra_1", "temporal-dev_temporal-web_1", "temporal-dev_temporal_1" } }); // Reset CadenceClient to its initial state. this.noReset = noReset; if (!noReset) { CadenceClient.Reset(); } // Start the Cadence container. base.StartAsComposed(name, composeFile, keepRunning); // It can take Cadence server some time to start. Rather than relying on [cadence-proxy] // to handle retries (which may take longer than the connect timeout), we're going to wait // up to 4 minutes for Temporal to start listening on its RPC socket. var retry = new LinearRetryPolicy(e => true, maxAttempts: int.MaxValue, retryInterval: TimeSpan.FromSeconds(0.5), timeout: TimeSpan.FromMinutes(4)); retry.Invoke( () => { // The [socket.Connect()] calls below will throw [SocketException] until // Temporal starts listening on its RPC socket. var socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp); socket.Connect(IPAddress.IPv6Loopback, NetworkPorts.Cadence); socket.Close(); }); Thread.Sleep(TimeSpan.FromSeconds(5)); // Wait a bit longer for luck! // Initialize the settings. settings = settings ?? new CadenceSettings() { CreateDomain = true, DefaultDomain = defaultDomain, LogLevel = logLevel }; if (settings.Servers.Count == 0) { settings.Servers.Add($"http://localhost:{NetworkPorts.Cadence}"); } this.settings = settings; this.reconnect = reconnect; if (!noClient) { // Establish the Cadence connection via the cadence proxy. Client = CadenceClient.ConnectAsync(settings).Result; HttpClient = new HttpClient() { BaseAddress = Client.ListenUri }; } } }