chore: Update project files and configurations; enhance .gitignore, add Qodana and SonarScanner scripts, and improve test project references

This commit is contained in:
Miha Kralj
2025-12-03 09:27:29 -08:00
parent 1d145d0622
commit 4a0a8d6da2
28 changed files with 257 additions and 88 deletions
+85
View File
@@ -0,0 +1,85 @@
#Requires -Version 7.0
<#
.SYNOPSIS
Static Analysis Scanner for QuanTAlib
.DESCRIPTION
Runs SonarCloud and Qodana with code coverage on Windows
#>
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Load tokens from User environment if not in process environment
if (-not $env:SONAR_TOKEN) {
$env:SONAR_TOKEN = [System.Environment]::GetEnvironmentVariable("SONAR_TOKEN", "User")
}
if (-not $env:QODANA_TOKEN) {
$env:QODANA_TOKEN = [System.Environment]::GetEnvironmentVariable("QODANA_TOKEN", "User")
}
# Verify tokens are available
if (-not $env:SONAR_TOKEN) {
Write-Error "SONAR_TOKEN environment variable not set"
exit 1
}
if (-not $env:QODANA_TOKEN) {
Write-Error "QODANA_TOKEN environment variable not set"
exit 1
}
# Coverage output directory for Qodana
$CoverageDir = ".qodana/code-coverage"
New-Item -ItemType Directory -Force -Path $CoverageDir | Out-Null
# Start SonarScanner analysis context
Write-Host "==> Starting SonarScanner analysis..." -ForegroundColor Cyan
dotnet-sonarscanner begin `
/o:"mihakralj-quantalib" `
/k:"mihakralj_QuanTAlib" `
/d:sonar.token="$env:SONAR_TOKEN" `
/d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
# Build solution (within SonarScanner context)
Write-Host "==> Building solution..." -ForegroundColor Cyan
dotnet build --no-incremental
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
# Run tests with coverage (opencover format works for both SonarCloud and can be converted)
Write-Host "==> Running tests with coverage..." -ForegroundColor Cyan
dotnet test --no-build --collect:"XPlat Code Coverage" `
-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover,lcov
# Continue even if tests fail - we still want coverage data for analysis
# Copy coverage files to Qodana directory with unique names
Write-Host "==> Copying coverage files to Qodana directory..." -ForegroundColor Cyan
$coverageFiles = Get-ChildItem -Recurse -Filter "coverage.info" -ErrorAction SilentlyContinue
if ($coverageFiles) {
$index = 0
foreach ($file in $coverageFiles) {
$destName = "coverage_$index.info"
Copy-Item $file.FullName -Destination (Join-Path $CoverageDir $destName) -Force
Write-Host " Copied: $($file.FullName) -> $destName" -ForegroundColor Gray
$index++
}
} else {
Write-Host " WARNING: No coverage.info files found" -ForegroundColor Yellow
}
# End SonarScanner analysis
Write-Host "==> Completing SonarScanner analysis..." -ForegroundColor Cyan
dotnet-sonarscanner end /d:sonar.token="$env:SONAR_TOKEN"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host "==> SonarCloud: https://sonarcloud.io/project/overview?id=mihakralj_QuanTAlib" -ForegroundColor Green
# Run Qodana
Write-Host "==> Starting Qodana analysis..." -ForegroundColor Cyan
# Set CI environment to suppress interactive prompts
$env:CI = "true"
qodana scan --within-docker=false -l qodana-dotnet --coverage-dir $CoverageDir
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host "==> Qodana analysis complete" -ForegroundColor Green
Write-Host "==> Done!" -ForegroundColor Green
+4
View File
@@ -413,3 +413,7 @@ ilspy/
#Ignore insiders AI rules
.github/instructions/codacy.instructions.md
#Ignore vscode AI rules
.github\instructions\codacy.instructions.md
+3 -1
View File
@@ -170,7 +170,9 @@
"sonarlint.connectedMode.project": {
"connectionId": "mihakralj-quantalib",
"projectKey": "mihakralj_QuanTAlib"
}
},
"qodana.projectId": "KbxmN"
// Note: Native mode is configured in qodana.yaml with withinDocker: false
// ???????????????????????????????????????????????????????????????????
// Keyboard Shortcuts Reference
+1 -2
View File
@@ -44,12 +44,11 @@
</PropertyGroup>
<PropertyGroup>
<NoWarn>S1144,S1944,S2053,S2222,S2259,S2583,S2589,S3329,S3655,S3776,S3900,S3949,S3966,S4158,S4347,S5773,S6781</NoWarn>
<NoWarn>S1144,S1944,S2053,S2222,S2245,S2259,S2583,S2589,S3329,S3655,S3776,S3900,S3949,S3966,S4158,S4347,S5773,S6781</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
<PackageReference Include="Microsoft.DotNet.Interactive.Formatting" Version="1.0.0-beta.25323.1" />
</ItemGroup>
<PropertyGroup Condition="'$(IsLocalBuild)' == 'true' AND $([MSBuild]::IsOSPlatform('Windows'))">
+11 -5
View File
@@ -1,25 +1,31 @@
workflow: GitHubFlow/v1
assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatch
tag-prefix: '[vV]'
major-version-bump-message: '\+semver:\s?(breaking|major)'
minor-version-bump-message: '\+semver:\s?(feature|minor)'
patch-version-bump-message: '\+semver:\s?(fix|patch)'
no-bump-message: '\+semver:\s?(none|skip)'
tag-prefix: '[vV]'
branches:
main:
regex: ^main$
mode: ContinuousDeployment
label: ''
increment: Patch
prevent-increment:
of-merged-branch: true
track-merge-target: false
is-release-branch: true
is-main-branch: true
pre-release-weight: 0
develop:
regex: ^dev(elop)?(ment)?$
mode: ContinuousDelivery
label: alpha
increment: Patch
track-merge-target: true
is-release-branch: false
source-branches: ['main']
track-merge-message: true
regex: ^dev(elop)?(ment)?$
source-branches:
- main
pre-release-weight: 30000
ignore:
sha: []
+2
View File
@@ -22,6 +22,8 @@
<ItemGroup>
<Using Include="Xunit" />
<Using Include="QuanTAlib" />
<Using Include="System.Collections" />
</ItemGroup>
<ItemGroup>
-4
View File
@@ -1,7 +1,3 @@
using System;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class EmaTests
+3 -5
View File
@@ -4,9 +4,7 @@ using System.Linq;
using Skender.Stock.Indicators;
using TALib;
using Tulip;
using Xunit;
using Xunit.Abstractions;
using QuanTAlib;
namespace QuanTAlib.Tests;
@@ -213,7 +211,7 @@ public class EmaValidationTests
// Calculate Tulip EMA
var emaIndicator = Tulip.Indicators.ema;
double[][] inputs = { tData };
double[] options = { (double)period };
double[] options = { period };
double[][] outputs = { new double[tData.Length] };
emaIndicator.Run(inputs, options, outputs);
@@ -246,7 +244,7 @@ public class EmaValidationTests
// Calculate Tulip EMA
var emaIndicator = Tulip.Indicators.ema;
double[][] inputs = { tData };
double[] options = { (double)period };
double[] options = { period };
double[][] outputs = { new double[tData.Length] };
emaIndicator.Run(inputs, options, outputs);
@@ -275,7 +273,7 @@ public class EmaValidationTests
// Calculate Tulip EMA
var emaIndicator = Tulip.Indicators.ema;
double[][] inputs = { sourceData };
double[] options = { (double)period };
double[] options = { period };
double[][] outputs = { new double[sourceData.Length] };
emaIndicator.Run(inputs, options, outputs);
+16 -3
View File
@@ -25,12 +25,25 @@ namespace QuanTAlib;
/// </remarks>
public class Ema
{
private struct State
private struct State : IEquatable<State>
{
public double Ema;
public double E;
public bool IsHot;
public static State New() => new() { Ema = 0, E = 1.0, IsHot = false };
public readonly bool Equals(State other) =>
Ema == other.Ema && E == other.E && IsHot == other.IsHot;
public override readonly bool Equals(object? obj) =>
obj is State other && Equals(other);
public override readonly int GetHashCode() =>
HashCode.Combine(Ema, E, IsHot);
public static bool operator ==(State left, State right) => left.Equals(right);
public static bool operator !=(State left, State right) => !left.Equals(right);
}
private readonly double _alpha;
@@ -60,7 +73,7 @@ public class Ema
/// <summary>
/// Creates EMA with specified alpha smoothing factor.
/// </summary>
/// <param name="alpha">Smoothing factor (0 < alpha <= 1)</param>
/// <param name="alpha">Smoothing factor (0 &lt; alpha &lt;= 1)</param>
public Ema(double alpha)
{
if (alpha <= 0 || alpha > 1)
@@ -217,7 +230,7 @@ public class Ema
/// </summary>
/// <param name="source">Input values</param>
/// <param name="output">Output span (must be same length as source)</param>
/// <param name="alpha">Smoothing factor (0 < alpha <= 1)</param>
/// <param name="alpha">Smoothing factor (0 &lt; alpha &lt;= 1)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double alpha)
{
-4
View File
@@ -1,7 +1,3 @@
using System;
using System.Linq;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests;
-4
View File
@@ -1,7 +1,3 @@
using System;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class SmaTests
+3 -5
View File
@@ -4,9 +4,7 @@ using System.Linq;
using Skender.Stock.Indicators;
using TALib;
using Tulip;
using Xunit;
using Xunit.Abstractions;
using QuanTAlib;
namespace QuanTAlib.Tests;
@@ -213,7 +211,7 @@ public class SmaValidationTests
// Calculate Tulip SMA
var smaIndicator = Tulip.Indicators.sma;
double[][] inputs = { tData };
double[] options = { (double)period };
double[] options = { period };
int lookback = period - 1;
double[][] outputs = { new double[tData.Length - lookback] };
@@ -247,7 +245,7 @@ public class SmaValidationTests
// Calculate Tulip SMA
var smaIndicator = Tulip.Indicators.sma;
double[][] inputs = { tData };
double[] options = { (double)period };
double[] options = { period };
int lookback = period - 1;
double[][] outputs = { new double[tData.Length - lookback] };
@@ -277,7 +275,7 @@ public class SmaValidationTests
// Calculate Tulip SMA
var smaIndicator = Tulip.Indicators.sma;
double[][] inputs = { sourceData };
double[] options = { (double)period };
double[] options = { period };
int lookback = period - 1;
double[][] outputs = { new double[sourceData.Length - lookback] };
-3
View File
@@ -1,6 +1,3 @@
using System.Linq;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests;
-4
View File
@@ -1,7 +1,3 @@
using System;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class WmaTests
+3 -5
View File
@@ -4,9 +4,7 @@ using System.Linq;
using Skender.Stock.Indicators;
using TALib;
using Tulip;
using Xunit;
using Xunit.Abstractions;
using QuanTAlib;
namespace QuanTAlib.Tests;
@@ -213,7 +211,7 @@ public class WmaValidationTests
// Calculate Tulip WMA
var wmaIndicator = Tulip.Indicators.wma;
double[][] inputs = { tData };
double[] options = { (double)period };
double[] options = { period };
int lookback = period - 1;
double[][] outputs = { new double[tData.Length - lookback] };
@@ -247,7 +245,7 @@ public class WmaValidationTests
// Calculate Tulip WMA
var wmaIndicator = Tulip.Indicators.wma;
double[][] inputs = { tData };
double[] options = { (double)period };
double[] options = { period };
int lookback = period - 1;
double[][] outputs = { new double[tData.Length - lookback] };
@@ -277,7 +275,7 @@ public class WmaValidationTests
// Calculate Tulip WMA
var wmaIndicator = Tulip.Indicators.wma;
double[][] inputs = { sourceData };
double[] options = { (double)period };
double[] options = { period };
int lookback = period - 1;
double[][] outputs = { new double[sourceData.Length - lookback] };
-3
View File
@@ -1,6 +1,3 @@
using System.Linq;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests;
-4
View File
@@ -1,7 +1,3 @@
using System;
using System.Collections;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests;
+20 -4
View File
@@ -423,7 +423,7 @@ public sealed class RingBuffer : IEnumerable<double>
/// <summary>
/// High-performance enumerator for the RingBuffer.
/// </summary>
public struct Enumerator : IEnumerator<double>
public struct Enumerator : IEnumerator<double>, IEquatable<Enumerator>
{
private readonly RingBuffer _buffer;
private readonly int _start;
@@ -453,8 +453,8 @@ public sealed class RingBuffer : IEnumerable<double>
return true;
}
public double Current => _current;
object IEnumerator.Current => Current;
public readonly double Current => _current;
readonly object IEnumerator.Current => Current;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
@@ -463,6 +463,22 @@ public sealed class RingBuffer : IEnumerable<double>
_current = default;
}
public void Dispose() { }
public readonly void Dispose() { }
public readonly bool Equals(Enumerator other) =>
ReferenceEquals(_buffer, other._buffer) &&
_start == other._start &&
_count == other._count &&
_index == other._index &&
_current == other._current;
public override readonly bool Equals(object? obj) =>
obj is Enumerator other && Equals(other);
public override readonly int GetHashCode() =>
HashCode.Combine(RuntimeHelpers.GetHashCode(_buffer), _start, _count, _index, _current);
public static bool operator ==(Enumerator left, Enumerator right) => left.Equals(right);
public static bool operator !=(Enumerator left, Enumerator right) => !left.Equals(right);
}
}
+4 -5
View File
@@ -1,6 +1,3 @@
using System;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests;
@@ -550,11 +547,13 @@ public class SimdExtensionsTests
Assert.True(avg > 0);
Assert.True(min > 0);
Assert.True(max > min);
Assert.Equal(min, minAlt);
Assert.Equal(max, maxAlt);
Assert.True(variance > 0);
Assert.True(stdDev > 0);
Assert.True(sw.ElapsedMilliseconds < 10,
$"SIMD operations took {sw.ElapsedMilliseconds}ms, expected < 10ms");
Assert.True(sw.ElapsedMilliseconds < 50,
$"SIMD operations took {sw.ElapsedMilliseconds}ms, expected < 50ms");
}
[Fact]
-3
View File
@@ -1,6 +1,3 @@
using System;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests
{
-6
View File
@@ -1,9 +1,3 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests
{
-6
View File
@@ -1,9 +1,3 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests
{
-3
View File
@@ -1,6 +1,3 @@
using System;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests
{
-1
View File
@@ -1,4 +1,3 @@
using Xunit;
namespace QuanTAlib.Tests;
-3
View File
@@ -1,6 +1,3 @@
using System;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests;
+5 -5
View File
@@ -32,17 +32,17 @@
<UserSecretsId>6afc11a7-4355-4f5e-9fdf-22431e5b03cb</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="QuanTAlib.Tests" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="QuanTAlib.Tests" />
</ItemGroup>
<ItemGroup>
<Compile Include="**\*.cs" Exclude="**\*.Tests.cs;**\*.Quantower.cs;obj\**\*.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" Version="5.12.0">
<PackageReference Include="GitVersion.MsBuild" Version="6.5.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
+96
View File
@@ -0,0 +1,96 @@
#-------------------------------------------------------------------------------#
# Qodana analysis is configured by qodana.yaml file #
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
#-------------------------------------------------------------------------------#
version: "1.0"
#Specify inspection profile for code analysis
profile:
name: qodana.starter
#Enable inspections
#include:
# - name: <SomeEnabledInspectionId>
#Disable inspections
exclude:
# Disable XML documentation comment validation (allows unescaped < > in comments)
- name: XmlDocAnalyzer
- name: InvalidXmlDocComment
# Library project - public API properties are intentionally unused internally
- name: UnusedAutoPropertyAccessor.Global
# Style preference - fully qualified names used intentionally for clarity
- name: RedundantNameQualifier
- name: RCS1036 # Roslyn: Remove redundant empty line
- name: IDE0001 # Simplify name
- name: IDE0002 # Simplify member access
# HIGH-PERFORMANCE LIBRARY EXCLUSIONS
# ------------------------------------
# Flat namespace structure is intentional for this library
- name: CheckNamespace
# Float comparisons are intentional in financial calculations (checking 0.0, NaN, sentinel values)
- name: CompareOfFloatsByEqualityOperator
# Explicit default args improve code clarity and self-documentation
- name: RedundantArgumentDefaultValue
# Platform-specific optimizations (SIMD, intrinsics) are intentional
- name: CA1416 # Validate platform compatibility
# Public API unused internally - this is a library
- name: UnusedMember.Global
- name: MemberCanBePrivate.Global
- name: ClassNeverInstantiated.Global
- name: UnusedType.Global
# Redundant using directives - managed by IDE/build, not critical for library
- name: RedundantUsingDirective
- name: IDE0005 # Remove unnecessary using directives
- name: CS8019 # Unnecessary using directive
# Nullable warning suppressions - used intentionally for null safety patterns
- name: RedundantSuppressNullableWarningExpression
# Redundant type specifications - explicit types used for clarity/documentation
- name: RedundantTypeArgumentsOfMethod
- name: RedundantCast
- name: RedundantExplicitArrayCreation
# Unused local variables - often used in test setup or placeholder code (includes false positives for tuple deconstruction)
- name: UnusedVariable
- name: UnusedVariable.Compiler # False positive for tuple deconstruction
- name: CS0219 # Variable is assigned but never used
# Object initializer in using statement - false positive for simple property setters
- name: CA2000 # Dispose objects before losing scope (overly cautious for simple cases)
- name: UseObjectOrCollectionInitializerWhenPossible
- name: DoNotUseObjectInitializerForUsingVariable
- name: ObjectInitializerMightCauseException
- name: ObjectCreationAsStatement
- name: UseObjectOrCollectionInitializer
- name: UsingStatementResourceInitialization # "Do not use object initializer for 'using' variable"
# Private field can be local variable - test fixtures often use fields for clarity/organization
- name: PrivateFieldCanBeConvertedToLocalVariable
- name: ConvertToLocalFunction
# Code coverage checks - SonarCloud handles coverage, Qodana coverage unreliable
- name: CoverageCheck
- name: ClassCoverageCheck
- name: MethodCoverageCheck
- name: CodeCoverageCheck
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
#bootstrap: sh ./prepare-qodana.sh
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
#plugins:
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
linter: qodana-dotnet
# Use native mode instead of Docker (avoids Unix socket issues on Windows)
withinDocker: false
+1
View File
@@ -10,6 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.0">