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
41 changes: 29 additions & 12 deletions src/LogExpert.Core/Classes/Log/RolloverFilenameBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,23 @@ public string BuildFileName ()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A saved mask *$J(.).log previously behaved as *$J(.) and matched app.log.1; it now means app.1.log. That's what the spec wants, but existing user settings change

if (_indexGroup != null && _indexGroup.Success)
{
fileName = fileName.Remove(_indexGroup.Index, _indexGroup.Length);
var indexPosition = _indexGroup.Index;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hidden invariant / Feature Envy — indexPosition = _condGroup.Index; indexLength += _condGroup.Length; silently assumes cond is contiguous and immediately precedes index — an invariant set 60 lines away in ParseFormatString. Uncommented; changing either regex breaks the other.

var indexLength = _indexGroup.Length;
if (_condGroup != null && _condGroup.Success)
{
indexPosition = _condGroup.Index;
indexLength += _condGroup.Length;
}

fileName = fileName.Remove(indexPosition, indexLength);

if (!_hideZeroIndex || Index > 0)
{
var format = "D" + _indexGroup.Length;
fileName = fileName.Insert(_indexGroup.Index, Index.ToString(format));
fileName = fileName.Insert(indexPosition, Index.ToString(format));
if (_hideZeroIndex && _condContent != null)
{
fileName = fileName.Insert(_indexGroup.Index, _condContent);
fileName = fileName.Insert(indexPosition, _condContent);
}
}
}
Expand All @@ -141,14 +149,14 @@ public string BuildFileName ()
private void ParseFormatString (string formatString)
{
var fmt = EscapeNonvarRegions(formatString);
var datePos = formatString.IndexOf("$D(", StringComparison.Ordinal);
var datePos = fmt.IndexOf("$D(", StringComparison.Ordinal);

if (datePos != -1)
{
var endPos = formatString.IndexOf(')', datePos);
var endPos = fmt.IndexOf(')', datePos);
if (endPos != -1)
{
_dateTimeFormat = formatString.Substring(datePos + 3, endPos - datePos - 3)
_dateTimeFormat = fmt.Substring(datePos + 3, endPos - datePos - 3)
.ToUpperInvariant()
.Replace('D', 'd')
.Replace('Y', 'y');
Expand Down Expand Up @@ -176,12 +184,15 @@ private void ParseFormatString (string formatString)
}
}

fmt = fmt.Replace("*", ".*", StringComparison.Ordinal);
fmt = fmt.Replace("*", ".*?", StringComparison.Ordinal);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.→.? and :194 \A…\z anchoring change every mask, not just ones with trailing literals. It holds because RolloverFilenameHandler.cs:39 passes a bare filename, not a path — but that invariant is undocumented and untested; a full path with any non-*-prefixed mask now fails where it previously matched.

_hideZeroIndex = fmt.Contains("$J", StringComparison.Ordinal);
fmt = fmt.Replace("$I", "(?'index'[\\d]+)", StringComparison.Ordinal);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Allow index tags ($J, $I) to appear before a fixed file extension suffix." RolloverFilenameBuilder.cs:189 still maps $I → (?'index'[\d]+); only $J gets the prefix-aware alternation (:190-193). So *$I.log cannot match the active file app.log — SetFileName fails, IsIndexPattern stays false, no chain is built. The one new $I test (RollingNameTest.cs:36, engine1.log→engine2.log) starts from a numbered file and never exercises the failing case.

fmt = fmt.Replace("$J", "(?'index'[\\d]*)", StringComparison.Ordinal);
var optionalIndexPattern = _condContent != null

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optionalIndexPattern reads as "the pattern is optional";

? $"(?:(?'cond'{Regex.Escape(_condContent)})(?'index'[\\d]+)|(?'index'))"
: "(?'index'[\\d]*)";
fmt = fmt.Replace("$J", optionalIndexPattern, StringComparison.Ordinal);

_regex = new Regex(fmt);
_regex = new Regex(@"\A" + fmt + @"\z");
}

private string EscapeNonvarRegions (string formatString)
Expand All @@ -192,15 +203,20 @@ private string EscapeNonvarRegions (string formatString)
StringBuilder result = new();
StringBuilder segment = new();

void FlushEscaped ()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FlushEscaped() covers only case 0, while case 1 and case 3 still inline _ = result.Append(segment); segment = new StringBuilder();.

{
_ = result.Append(Regex.Escape(segment.ToString()));
segment = new StringBuilder();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extract it to a real function not a function in a function, this only leads to unreadable code


for (var i = 0; i < fmt.Length; ++i)
{
switch (state)
{
case 0: // looking for $
if (fmt[i] == '$')
{
_ = result.Append(Regex.Escape(segment.ToString()));
segment = new StringBuilder();
FlushEscaped();
state = 1;
}

Expand Down Expand Up @@ -238,9 +254,10 @@ private string EscapeNonvarRegions (string formatString)
}
}

FlushEscaped();
fmt = result.ToString().Replace('\xFFFD', '*');
return fmt;
}

#endregion
}
}
10 changes: 7 additions & 3 deletions src/LogExpert.Resources/Resources.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1680,12 +1680,16 @@ Ein ausgewähltes Tool erscheint in der Iconbar. Alle anderen verfügbaren Tools
<value>Muster syntax:

* = alle Zeichen (wildcard)
$D(&amp;lt;date&amp;gt;) = Datumsmuster
$D(&lt;date&gt;) = Datumsmuster
$I = Dateiindexnummer
$J = Dateiindexnummer, versteckt wenn 09

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still reads $J = Dateiindexnummer, versteckt wenn 09 — should be 0. Correct in the English and Chinese files.

$J(&amp;lt;prefix&amp;gt;) = Wie $J, jedoch wird ein &amp;lt;prefix&amp;gt; hinzugefügt when es nicht 0 ist
$J(&lt;prefix&gt;) = Wie $J, jedoch wird ein &lt;prefix&gt; hinzugefügt wenn es nicht 0 ist

&amp;lt;date&amp;gt;:
Beispiele:
*$J(.) → app.log, app.log.1, app.log.2
*$J(.).log → app.log, app.1.log, app.2.log

&lt;date&gt;:
DD = Tag
MM = Monat
YY[YY] = Jahr
Expand Down
10 changes: 7 additions & 3 deletions src/LogExpert.Resources/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1743,12 +1743,16 @@ Checked tools will appear in the icon bar. All other tools are available in the
<value>Pattern syntax:

* = any characters (wildcard)
$D(&amp;lt;date&amp;gt;) = Date pattern
$D(&lt;date&gt;) = Date pattern
$I = File index number
$J = File index number, hidden when zero
$J(&amp;lt;prefix&amp;gt;) = Like $J, but adding &amp;lt;prefix&amp;gt; when non-zero
$J(&lt;prefix&gt;) = Like $J, but adding &lt;prefix&gt; when non-zero

&amp;lt;date&amp;gt;:
Examples:
*$J(.) → app.log, app.log.1, app.log.2
*$J(.).log → app.log, app.1.log, app.2.log

&lt;date&gt;:
DD = day
MM = month
YY[YY] = year
Expand Down
4 changes: 4 additions & 0 deletions src/LogExpert.Resources/Resources.zh-CN.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1554,6 +1554,10 @@ $I = 文件索引编号
$J = 文件索引编号,为零时隐藏
$J(&lt;前缀&gt;) = 类似 $J,但在非零时添加 &lt;前缀&gt;

示例:
*$J(.) → app.log, app.log.1, app.log.2
*$J(.).log → app.log, app.1.log, app.2.log

&lt;日期&gt;:
DD = 日
MM = 月
Expand Down
6 changes: 6 additions & 0 deletions src/LogExpert.Tests/LogExpert.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@
<Content Include="TestData\app.log.1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="TestData\app.1.log">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="TestData\app.2.log">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<None Update="TestData\organizations-1000.csv">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
18 changes: 17 additions & 1 deletion src/LogExpert.Tests/RollingNameTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public void TestFilename1(string expectedResult, string formatString)
[TestCase("engine.log", "engine1.log","engine$J.log")]
[TestCase("engine1.log", "engine2.log","engine$J.log")]
[TestCase("engine.log", "engine.log.1","*$J(.)")]
[TestCase("engine.log", "engine.1.log", "*$J(.).log")]
[TestCase("engine.log.1", "engine.log.2", "*$J(.)")]
[TestCase("engine.1.log", "engine.2.log", "*$J(.).log")]
[TestCase("engine1.log", "engine2.log", "*$I.log")]
[TestCase("engine_2010-06-12.1.log", "engine_2010-06-12.2.log", "*$D(yyyy-MM-dd)$J(.).log")]
[TestCase("engine_2010-06-12.log", "engine_2010-06-12.log.1", "*$D(yyyy-MM-dd).log$J(.)")]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$I untested. For "$J, $I" support, the fix itself generalizes, but no test covers $I or $D followed by a trailing literal (e.g. *$D(yyyy-MM-dd)$J(.).log).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added cases which cover:

  • $I with a trailing literal .log, starting from a rotated file.
  • $D plus $J(.) followed by a trailing .log, also starting from a rotated file.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ParseFormatString computes datePos from the original string but edits the escaped one — a literal with regex metachars before $D (e.g. app.$D(yyyy-MM-dd)) corrupts the regex. Not introduced here, but the new trailing-literal support makes such patterns likelier.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by locating and parsing $D(...) in the escaped format string. Offsets remain correct when literal regex metacharacters appear before the date placeholder.
Added a regression test for app.$D(yyyy-MM-dd).log that verifies date parsing and rebuilding.

public void TestFilenameAnd1(string fileName, string expectedResult, string formatString)
{
Expand All @@ -45,6 +50,7 @@ public void TestFilenameAnd1(string fileName, string expectedResult, string form
[Test]
[TestCase("engine.log", "engine.log.2","*$J(.)")]
[TestCase("engine.log", "engine.log.2","*.log$J(.)")]
[TestCase("engine.log", "engine.2.log", "*$J(.).log")]
public void TestFilenameAnd2(string fileName, string expectedResult, string formatString)
{
RolloverFilenameBuilder fnb = new(formatString);
Expand All @@ -54,6 +60,16 @@ public void TestFilenameAnd2(string fileName, string expectedResult, string form
Assert.That(name, Is.EqualTo(expectedResult));
}

[Test]
public void BuildFileName_DatePatternAfterRegexMetacharacter_IncrementsDate ()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BuildFileName_X_Y while the file's existing methods are TestFilenameAnd1/2.

{
RolloverFilenameBuilder fnb = new("app.$D(yyyy-MM-dd).log");
fnb.SetFileName("app.2010-06-12.log");

fnb.IncrementDate();

Assert.That(fnb.BuildFileName(), Is.EqualTo("app.2010-06-13.log"));
}

[Test]
[TestCase("engine1.log", "engine.log","engine$J.log")]
Expand All @@ -65,4 +81,4 @@ public void TestFilenameMinus1(string fileName, string expectedResult, string fo
var name = fnb.BuildFileName();
Assert.That(name, Is.EqualTo("engine.log"));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public void SetUp ()

File.Copy(Path.Combine(_testDataDirectory, "app.log"), _logFile);
File.Copy(Path.Combine(_testDataDirectory, "app.log.1"), _logFile + ".1");
File.Copy(Path.Combine(_testDataDirectory, "app.1.log"), Path.Combine(_testDirectory, "app.1.log"));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SetUp copies app.1.log/app.2.log unconditionally, so all four tests now see two extra files though only the new one needs them.

File.Copy(Path.Combine(_testDataDirectory, "app.2.log"), Path.Combine(_testDirectory, "app.2.log"));

_ = PluginRegistry.PluginRegistry.Create(_testDirectory, 500);
}
Expand Down Expand Up @@ -68,6 +70,22 @@ public void SingleFileCtor_MultiFileTrue_ExpandsRollover ()
});
}

[Test]
public void SingleFileCtor_MultiFileTrue_LoadsIndexBeforeExtension ()
{
var options = new MultiFileOptions { FormatPattern = "*$J(.).log" };
using var reader = CreateSingleFileReader(multiFile: true, options);

reader.ReadFiles();

Assert.Multiple(() =>
{
Assert.That(reader.IsMultiFile, Is.True);
Assert.That(reader.GetLogFileInfoList().Select(file => Path.GetFileName(file.FullName)),
Is.EqualTo(new[] { "app.2.log", "app.1.log", "app.log" }));
});
}

[Test]
public void MultiFileCtor_AlwaysMultiFile ()
{
Expand All @@ -91,15 +109,15 @@ public void MultiFileCtor_AlwaysMultiFile ()
});
}

private LogfileReader CreateSingleFileReader (bool multiFile)
private LogfileReader CreateSingleFileReader (bool multiFile, MultiFileOptions? options = null)
{
return new LogfileReader(
_logFile,
new EncodingOptions { Encoding = Encoding.UTF8 },
multiFile,
bufferCount: 40,
linesPerBuffer: 50,
new MultiFileOptions(),
options ?? new MultiFileOptions(),
ReaderType.System,
PluginRegistry.PluginRegistry.Instance,
maximumLineLength: 500,
Expand Down
1 change: 1 addition & 0 deletions src/LogExpert.Tests/TestData/app.1.log
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
app.1.log
1 change: 1 addition & 0 deletions src/LogExpert.Tests/TestData/app.2.log
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
app.2.log

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions src/LogExpert.UI/Dialogs/MultiFileMaskDialog.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading