Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0-noble

RUN apt-get update \
&& export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends git octave \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
8 changes: 8 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "Octave.NET",
"build": {
"context": "..",
"dockerfile": "Dockerfile"
},
"postCreateCommand": "dotnet --info && octave-cli --version && dotnet restore src/Octave.NET.sln"
}
22 changes: 10 additions & 12 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
language: csharp
dist: trusty
mono: none
dotnet: 2.0.0
before_install:
- sudo add-apt-repository ppa:octave/stable -y
- sudo apt-get update -y
- sudo apt-get install octave -y
language: minimal
dist: jammy

install:
- cd src
- dotnet restore
services:
- docker

env:
global:
- DEVCONTAINER_IMAGE=octave-net-ci

script:
- dotnet test Octave.NET.Tests
- docker build -f .devcontainer/Dockerfile -t "$DEVCONTAINER_IMAGE" .
- docker run --rm -v "$TRAVIS_BUILD_DIR:/workspace" -w /workspace "$DEVCONTAINER_IMAGE" dotnet test src/Octave.NET.Tests/Octave.NET.Tests.csproj --logger "console;verbosity=minimal"
11 changes: 11 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project>
<PropertyGroup>
<_BuildOperatingSystem Condition="$([MSBuild]::IsOSPlatform('Windows'))">windows</_BuildOperatingSystem>
<_BuildOperatingSystem Condition="$([MSBuild]::IsOSPlatform('Linux'))">linux</_BuildOperatingSystem>
<_BuildOperatingSystem Condition="'$(_BuildOperatingSystem)' == ''">unknown</_BuildOperatingSystem>

<BaseOutputPath>bin/$(_BuildOperatingSystem)/</BaseOutputPath>
<BaseIntermediateOutputPath>obj/$(_BuildOperatingSystem)/</BaseIntermediateOutputPath>
<DefaultItemExcludes>$(DefaultItemExcludes);bin/**;obj/**</DefaultItemExcludes>
</PropertyGroup>
</Project>
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ mathematical problems. This library is an attempt to bridge Octave and .NET worl
or specify path to octave-cli binary in your code
- Check the 'examples' folder

## Security notes

Octave.NET sends commands directly to an `octave-cli` process. Treat every string passed to `Execute` as executable code. Do not pass untrusted user input directly to Octave unless your application validates it, authorizes it, and accepts the risks of Octave code being able to read files, write files, load packages, or invoke system commands.

For production services, prefer setting `OctaveContext.OctaveSettings.OctaveCliPath` to an absolute path that you control instead of relying on `PATH` lookup. Relying on `PATH` can start an unexpected executable if the process environment is misconfigured or writable by another user.

Worker processes are pooled for performance. Disposing an `OctaveContext` returns the underlying Octave process to the pool, and Octave variables are not cleared automatically. Do not store secrets in Octave variables unless you clear them before disposing the context, for example by executing `clear`. For workloads that must isolate data between users or requests, consider using separate processes or explicitly clearing all state before returning a context.

The asynchronous `Execute(string command, CancellationToken cancellationToken)` overload depends on the supplied cancellation token. If the command never completes and the token is never canceled, the returned task can remain incomplete. Use a `CancellationTokenSource` with an appropriate timeout for untrusted, long-running, or user-controlled commands.

The repository's Dev Container and CI configuration use current package feeds to install .NET and Octave. That is convenient for development, but release pipelines with strict supply-chain requirements should pin image digests and review package updates regularly.


## How It's Made?
This library spawns octave processes and controls them via standard streams (stdin, stdout and stderr). To keep optimal performance every time OctaveContext is disposed underlying octave-cli process is returned to the object pool, so we don't waste time on spawning new worker processes.
Expand Down
226 changes: 226 additions & 0 deletions src/Octave.NET.Tests/ObjectPoolTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Octave.NET.Core.ObjectPooling;
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace Octave.NET.Tests
{
[TestClass]
public class ObjectPoolTests
{
private const int TestTimeoutMS = 5_000;

[TestMethod]
[Timeout(TestTimeoutMS)]
public void WhenReleasedItemCanBeReused_ShouldReturnSameItem()
{
var created = 0;

using (var pool = new ObjectPool<TestPoolItem>(() => new TestPoolItem(++created), 1_000, false, 1))
{
var item = pool.GetObject();

pool.ReleaseObject(item);

var reusedItem = pool.GetObject();

Assert.AreSame(item, reusedItem);
Assert.AreEqual(1, created);

pool.ReleaseObject(reusedItem);
}
}

[TestMethod]
[Timeout(TestTimeoutMS)]
public void WhenReleasedItemCannotBeReused_ShouldDisposeItAndCreateReplacement()
{
var created = 0;

using (var pool = new ObjectPool<TestPoolItem>(() => new TestPoolItem(++created), 1_000, false, 1))
{
var item = pool.GetObject();
item.CanBeReused = false;

pool.ReleaseObject(item);

Assert.IsTrue(item.IsDisposed);

var replacement = pool.GetObject();

Assert.AreNotSame(item, replacement);
Assert.AreEqual(2, created);

pool.ReleaseObject(replacement);
}
}

[TestMethod]
[Timeout(TestTimeoutMS)]
public async Task WhenPoolAtMaxConcurrency_ShouldWaitUntilLeaseIsReleased()
{
using (var pool = new ObjectPool<TestPoolItem>(() => new TestPoolItem(), 1_000, false, 1))
using (var waiterStarted = new ManualResetEventSlim(false))
{
var item = pool.GetObject();

var waitingGet = Task.Run(() =>
{
waiterStarted.Set();
return pool.GetObject();
});

Assert.IsTrue(waiterStarted.Wait(1_000));
Assert.IsFalse(waitingGet.Wait(100));

pool.ReleaseObject(item);

var acquiredItem = await WaitForTaskAsync(waitingGet).ConfigureAwait(false);

Assert.AreSame(item, acquiredItem);

pool.ReleaseObject(acquiredItem);
}
}

[TestMethod]
[Timeout(TestTimeoutMS)]
public async Task WhenFactoryThrows_ShouldReleaseLease()
{
var attempts = 0;

using (var pool = new ObjectPool<TestPoolItem>(() =>
{
var attempt = Interlocked.Increment(ref attempts);

if (attempt == 1)
throw new InvalidOperationException("Factory failed.");

return new TestPoolItem(attempt);
}, 1_000, false, 1))
{
Assert.ThrowsExactly<InvalidOperationException>(() => pool.GetObject());

var getTask = Task.Run(() => pool.GetObject());
var item = await WaitForTaskAsync(getTask).ConfigureAwait(false);

Assert.AreEqual(2, item.Id);

pool.ReleaseObject(item);
}
}

[TestMethod]
[Timeout(TestTimeoutMS)]
public void WhenDisposed_ShouldDisposePooledItemsAndRejectNewLeases()
{
var pool = new ObjectPool<TestPoolItem>(() => new TestPoolItem(), 1_000, false, 1);
var item = pool.GetObject();

pool.ReleaseObject(item);
pool.Dispose();

Assert.IsTrue(item.IsDisposed);
Assert.ThrowsExactly<ObjectDisposedException>(() => pool.GetObject());
}

[TestMethod]
[Timeout(TestTimeoutMS)]
public void WhenDisposed_ShouldDisposeCheckedOutItemWhenReturned()
{
var pool = new ObjectPool<TestPoolItem>(() => new TestPoolItem(), 1_000, false, 1);
var item = pool.GetObject();

pool.Dispose();
pool.ReleaseObject(item);

Assert.IsTrue(item.IsDisposed);
}

[TestMethod]
[Timeout(TestTimeoutMS)]
public async Task WhenIdleReclaimRuns_ShouldDisposeIdlePooledItem()
{
var created = 0;

using (var pool = new ObjectPool<TestPoolItem>(() => new TestPoolItem(++created), 10, true, 1, 50))
{
var item = pool.GetObject();

pool.ReleaseObject(item);

Assert.IsTrue(await WaitUntilAsync(() => item.IsDisposed).ConfigureAwait(false));

var replacementTask = Task.Run(() => pool.GetObject());
var replacement = await WaitForTaskAsync(replacementTask).ConfigureAwait(false);

Assert.AreNotSame(item, replacement);
Assert.AreEqual(2, created);

pool.ReleaseObject(replacement);
}
}

[TestMethod]
[Timeout(TestTimeoutMS)]
public void WhenDisposed_ShouldCancelReclaimTaskWithoutWaitingForInterval()
{
var pool = new ObjectPool<TestPoolItem>(() => new TestPoolItem(), 60_000, true, 1, 60_000);
var stopwatch = Stopwatch.StartNew();

pool.Dispose();

stopwatch.Stop();
Assert.IsTrue(stopwatch.ElapsedMilliseconds < 1_000);
}

private static async Task<TestPoolItem> WaitForTaskAsync(Task<TestPoolItem> task)
{
var completedTask = await Task.WhenAny(task, Task.Delay(1_000)).ConfigureAwait(false);

Assert.AreSame(task, completedTask);

return await task.ConfigureAwait(false);
}

private static async Task<bool> WaitUntilAsync(Func<bool> predicate)
{
var stopwatch = Stopwatch.StartNew();

while (stopwatch.ElapsedMilliseconds < 1_000)
{
if (predicate())
return true;

await Task.Delay(10).ConfigureAwait(false);
}

return predicate();
}

private sealed class TestPoolItem : IPoolable, IDisposable
{
public TestPoolItem()
{
}

public TestPoolItem(int id)
{
Id = id;
}

public int Id { get; }

public bool CanBeReused { get; set; } = true;

public bool IsDisposed { get; private set; }

public void Dispose()
{
IsDisposed = true;
}
}
}
}
8 changes: 4 additions & 4 deletions src/Octave.NET.Tests/Octave.NET.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.0" />
<PackageReference Include="MSTest.TestAdapter" Version="1.2.0" />
<PackageReference Include="MSTest.TestFramework" Version="1.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageReference Include="MSTest.TestAdapter" Version="4.2.1" />
<PackageReference Include="MSTest.TestFramework" Version="4.2.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading