diff --git a/.github/scripts/start-ncache.sh b/.github/scripts/start-ncache.sh new file mode 100644 index 0000000..4197e36 --- /dev/null +++ b/.github/scripts/start-ncache.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# +# Brings up a single-node NCache Open Source server in Docker and starts the cache +# that the sample connects to. Exports NCACHE_SERVER and NCACHE_CACHE_NAME through +# $GITHUB_ENV so the test step can reach the cache. +# +# Requires NCACHE_REGISTRATION_KEY and NCACHE_REGISTRATION_EMAIL: NCache Open Source +# refuses to start a cache until it has been registered, and the key is only accepted +# together with the email address it was issued to. + +set -euo pipefail + +IMAGE="${NCACHE_IMAGE:-alachisoft/ncache:latest-oss}" +CONTAINER="${NCACHE_CONTAINER:-ncache}" +CACHE_NAME="${NCACHE_CACHE_NAME:-demoCache}" +TOOLS="/opt/ncache/bin/tools" + +: "${NCACHE_REGISTRATION_KEY:?NCACHE_REGISTRATION_KEY is not set}" +: "${NCACHE_REGISTRATION_EMAIL:?NCACHE_REGISTRATION_EMAIL is not set}" + +docker pull "$IMAGE" + +# Host networking, not a published port. NCache binds to one address and hands that +# same address back to connecting clients, so a client that reached the server on a +# forwarded port is still redirected to the container's own address and fails. +docker run -d --name "$CONTAINER" --network host "$IMAGE" + +echo "Waiting for the NCache management service..." +for attempt in $(seq 1 30); do + if docker exec "$CONTAINER" "$TOOLS/get-caches" >/dev/null 2>&1; then + break + fi + + if [ "$attempt" -eq 30 ]; then + echo "::error::NCache management service did not come up in time." + docker logs "$CONTAINER" + exit 1 + fi + + sleep 2 +done + +# The tools report failures on stdout and do not always exit non-zero, so match on +# the success message rather than trusting the exit code. +echo "Registering NCache Open Source..." +registration=$(docker exec "$CONTAINER" "$TOOLS/register-ncacheopensource" \ + -Key "$NCACHE_REGISTRATION_KEY" \ + -Email "$NCACHE_REGISTRATION_EMAIL" \ + -FirstName "${NCACHE_REGISTRATION_FIRSTNAME:-GitHub}" \ + -LastName "${NCACHE_REGISTRATION_LASTNAME:-Actions}" \ + -Company "${NCACHE_REGISTRATION_COMPANY:-Alachisoft}" 2>&1 || true) +echo "$registration" + +if ! printf '%s' "$registration" | grep -qi "has been registered"; then + echo "::error::NCache registration failed. Check NCACHE_REGISTRATION_KEY and NCACHE_REGISTRATION_EMAIL." + exit 1 +fi + +echo "Starting cache '$CACHE_NAME'..." +start=$(docker exec "$CONTAINER" "$TOOLS/start-cache" -Name "$CACHE_NAME" 2>&1 || true) +echo "$start" + +if ! printf '%s' "$start" | grep -qi "successfully started"; then + echo "::error::Failed to start cache '$CACHE_NAME'." + docker logs "$CONTAINER" + exit 1 +fi + +docker exec "$CONTAINER" "$TOOLS/get-caches" + +# The container's startup script rewrites the .ncconf files with whichever address +# NCache bound to, and that is the address a client has to use. Read it back instead +# of assuming localhost. +server=$(docker exec "$CONTAINER" \ + sed -n 's/.*local-server-ip="\([^"]*\)".*/\1/p' /opt/ncache/config/client.ncconf | head -n 1 | tr -d '\r') + +if [ -z "$server" ]; then + echo "::error::Could not read local-server-ip from client.ncconf." + exit 1 +fi + +echo "NCache is listening on ${server}:9800" + +{ + echo "NCACHE_SERVER=${server}" + echo "NCACHE_CACHE_NAME=${CACHE_NAME}" +} >>"$GITHUB_ENV" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4e1d5e4 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,68 @@ +name: Build + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: + +# A new push to the same PR/branch cancels the run that is still in progress. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + name: Build solution + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + # The wrapper multi-targets netstandard2.0/net462 and the sample targets + # net8.0; the net462 reference assemblies come from NuGet during restore. + dotnet-version: 8.0.x + + - name: Build + run: dotnet build NCache.OSS.StackExchange.Redis.sln --configuration Release + + test: + name: Unit tests + # Linux, because the tests need a live NCache server and the NCache image is a + # Linux container. Windows runners cannot run it. + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Start NCache + env: + NCACHE_REGISTRATION_KEY: ${{ secrets.NCACHE_REGISTRATION_KEY }} + NCACHE_REGISTRATION_EMAIL: ${{ secrets.NCACHE_REGISTRATION_EMAIL }} + run: bash .github/scripts/start-ncache.sh + + # The sample picks up NCACHE_SERVER and NCACHE_CACHE_NAME from the environment + # and returns a non-zero exit code when any test fails or throws. + - name: Run tests + run: dotnet run --project NCache.StackExchange.Redis.Sample/NCache.OSS.StackExchange.Redis.Sample.csproj --configuration Release + + - name: NCache server logs + if: failure() + run: docker logs ncache + + - name: Stop NCache + if: always() + run: docker rm --force ncache diff --git a/NCache.StackExchange.Redis.Sample/Logger.cs b/NCache.StackExchange.Redis.Sample/Logger.cs index 5934bf9..d967314 100644 --- a/NCache.StackExchange.Redis.Sample/Logger.cs +++ b/NCache.StackExchange.Redis.Sample/Logger.cs @@ -36,6 +36,7 @@ internal static void PrintDataCacheException(Exception ex) Console.Write("\n\n"); } + Program.erroredTests++; Console.ForegroundColor = color; } diff --git a/NCache.StackExchange.Redis.Sample/Program.cs b/NCache.StackExchange.Redis.Sample/Program.cs index 417415f..a17b256 100644 --- a/NCache.StackExchange.Redis.Sample/Program.cs +++ b/NCache.StackExchange.Redis.Sample/Program.cs @@ -10,6 +10,7 @@ using NCache.OSS.StackExchange.Redis.Sample.LockOperations; using NCache.OSS.StackExchange.Redis.Sample.PubSubOperations; using System; +using System.Collections.Generic; using System.Configuration; namespace BasicUsageStackExchangeRedis @@ -19,30 +20,52 @@ class Program public static int successfulTests = 0; public static int failedTests = 0; + public static int erroredTests = 0; public static INCacheDatabase db; public static string myObjectForCaching = "This is my Object"; public static ICache cache; public static ConnectionMultiplexer mux; - static void Main(string[] args) + static int Main(string[] args) { try { InitializeClient(); RunAllTests(); - - Console.WriteLine($"Test completed successfully.\nSuccessful Tests: {successfulTests}\nFailed Tests: {failedTests}\nPress any key to exit..."); - Console.ReadKey(); } catch (Exception ex) { Console.WriteLine("Exception: " + ex.Message); + return 1; + } + + Console.WriteLine($"Test run completed.\nSuccessful Tests: {successfulTests}\nFailed Tests: {failedTests}\nErrored Tests: {erroredTests}"); + + WaitForKeyPress(); + + return failedTests + erroredTests == 0 ? 0 : 1; + } + + // for CI pipeline + private static void WaitForKeyPress() + { + if (Console.IsInputRedirected || Environment.GetEnvironmentVariable("CI") == "true") + { + return; } + + Console.WriteLine("Press any key to exit..."); + Console.ReadKey(); } private static void InitializeClient() { - string cacheName = ConfigurationManager.AppSettings["CacheId"]; + string cacheName = Environment.GetEnvironmentVariable("NCACHE_CACHE_NAME"); + + if (string.IsNullOrWhiteSpace(cacheName)) + { + cacheName = ConfigurationManager.AppSettings["CacheId"]; + } Microsoft.Extensions.Logging.ILoggerFactory loggerFactory = LoggerFactory.Create(builder => { @@ -51,7 +74,7 @@ private static void InitializeClient() .SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Debug); }); - mux = ConnectionMultiplexer.Connect(cacheName); + mux = ConnectionMultiplexer.Connect(cacheName, GetConnectionOptions()); cache = mux.GetNCacheInterface(cacheName); cache.Clear(); @@ -59,6 +82,26 @@ private static void InitializeClient() db = mux.GetDatabase(); } + private static CacheConnectionOptions GetConnectionOptions() + { + string server = Environment.GetEnvironmentVariable("NCACHE_SERVER"); + + if (string.IsNullOrWhiteSpace(server)) + { + return null; + } + + if (!int.TryParse(Environment.GetEnvironmentVariable("NCACHE_PORT"), out int port)) + { + port = 9800; + } + + return new CacheConnectionOptions + { + ServerList = new List { new ServerInfo(server, port) } + }; + } + private static void RunAllTests() { RunKeyOperations(); diff --git a/NCache.StackExchange.Redis/NCache.OSS.StackExchange.Redis.csproj b/NCache.StackExchange.Redis/NCache.OSS.StackExchange.Redis.csproj index 744f4ff..91bbe2a 100644 --- a/NCache.StackExchange.Redis/NCache.OSS.StackExchange.Redis.csproj +++ b/NCache.StackExchange.Redis/NCache.OSS.StackExchange.Redis.csproj @@ -19,6 +19,13 @@ + + + + + Always diff --git a/README.md b/README.md index 21d3ca5..4d27b45 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # NCache OSS Wrapper for StackExchange.Redis +[![Build](https://github.com/Alachisoft/Redis-Wrapper/actions/workflows/build.yml/badge.svg)](https://github.com/Alachisoft/Redis-Wrapper/actions/workflows/build.yml) + A compatibility layer that enables .NET applications to use [NCache](https://www.alachisoft.com/ncache/) as a drop-in replacement for Redis with the familiar StackExchange.Redis API. ## Package Versions @@ -92,6 +94,14 @@ ILoggerFactory loggerFactory = LoggerFactory.Create(builder => The application acts as a test suite, targeting all APIs defined within the wrapper. Simply plug in your cache and run Program.cs. +It prints a pass/fail/error tally and exits with a non-zero code if anything failed or threw, so it can be used as a build gate. By default it reads the cache name from `App.config` and the server list from `client.ncconf`; these environment variables override that, which is how the workflow points it at a cache it starts itself: + +| Variable | Purpose | +|---|---| +| `NCACHE_CACHE_NAME` | Cache to connect to, instead of the `CacheId` app setting | +| `NCACHE_SERVER` | Cache server address, instead of the `client.ncconf` server list | +| `NCACHE_PORT` | Client port, defaults to `9800` | + ## License Copyright © 2005-2026 Alachisoft. All rights reserved.