Skip to content
27 changes: 20 additions & 7 deletions Tests/IntegrationTests/Test.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,19 @@ public void ClassificationReturnsExpectedResult(Type testCaseType)
var classifier = classifierProvider.GetClassifier(new TextBufferStub(testCase.ContentType));

var actualResult = new List<ClassifiedText>();

var spansToReclassify = new List<SnapshotSpan>();
classifier.ClassificationChanged += (sender, args) => spansToReclassify.Add(args.ChangeSpan);

foreach (var line in testCase.SourceText)
{
var snapshot = new TextSnapshotStub(line);
var span = new SnapshotSpan(snapshot, new Span(0, snapshot.Length));
var classificationSpans = classifier.GetClassificationSpans(span);
foreach (var classificationSpan in classificationSpans)
var span = Utils.CreateSpan(line);
DoClassification(classifier, span, actualResult);
foreach (var spanToReclassify in spansToReclassify)
{
var classifiedText = classificationSpan.Span.GetText();
var classificationType = classificationSpan.ClassificationType.Classification;
actualResult.Add(new ClassifiedText(classificationType, classifiedText));
DoClassification(classifier, spanToReclassify, actualResult);
}
spansToReclassify.Clear();
}

actualResult.Should().BeEquivalentTo(testCase.ExpectedResult);
Expand All @@ -62,5 +64,16 @@ private IClassifierProvider CreateClassifierProvider()
var classifierProvider = container.GetExport<IClassifierProvider>();
return classifierProvider.Value;
}

private void DoClassification(IClassifier classifier, SnapshotSpan span, IList<ClassifiedText> result)
{
var classificationSpans = classifier.GetClassificationSpans(span);
foreach (var classificationSpan in classificationSpans)
{
var classifiedText = classificationSpan.Span.GetText();
var classificationType = classificationSpan.ClassificationType.Classification;
result.Add(new ClassifiedText(classificationType, classifiedText));
}
}
}
}
7 changes: 5 additions & 2 deletions Tests/IntegrationTests/TestCases/BuildOrderOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@ public class BuildOrderOutput : ITestCase
"2>------ Build started: Project: WebDemo, Configuration: Debug Any CPU ------\r\n",
"1>C:\\test\\ConsoleDemo\\Program.cs: warning CS0168: The variable 'ex' is declared but never used\r\n",
"1>C:\\test\\ConsoleDemo\\Program.cs(1,14,1,15): error CS1022: Type or namespace definition, or end-of-file expected\r\n",
"2> WebDemo -> C:\\test\\WebDemo\bin\\WebDemo.dll\r\n",
"2> WebDemo -> C:\\test\\WebDemo\\bin\\WebDemo.dll\r\n",
"========== Build: 1 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========\r\n"
};

public IReadOnlyList<ClassifiedText> ExpectedResult { get; } = new[]
{
new ClassifiedText(ClassificationType.BuildMessageWarning, "warning CS0168: The variable 'ex' is declared but never used"),
new ClassifiedText(ClassificationType.BuildActionStartedWarning,"------ Build started: Project: ConsoleDemo, Configuration: Debug Any CPU ------"),
new ClassifiedText(ClassificationType.BuildMessageError, "error CS1022: Type or namespace definition, or end-of-file expected"),
new ClassifiedText(ClassificationType.BuildResultFailed, "========== Build: 1 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========\r\n")
new ClassifiedText(ClassificationType.BuildActionStartedError,"------ Build started: Project: ConsoleDemo, Configuration: Debug Any CPU ------"),
new ClassifiedText(ClassificationType.BuildResultFailed, "========== Build: 1 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========\r\n"),
new ClassifiedText(ClassificationType.BuildActionStartedSuccess,"------ Build started: Project: WebDemo, Configuration: Debug Any CPU ------")
};
}
}
5 changes: 4 additions & 1 deletion Tests/IntegrationTests/TestCases/BuildOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ public class BuildOutput : ITestCase
public IReadOnlyList<ClassifiedText> ExpectedResult { get; } = new[]
{
new ClassifiedText(ClassificationType.BuildMessageWarning, "warning CS0168: The variable 'ex' is declared but never used"),
new ClassifiedText(ClassificationType.BuildActionStartedWarning, "------ Build started: Project: ConsoleDemo, Configuration: Debug Any CPU ------"),
new ClassifiedText(ClassificationType.BuildMessageError, "error CS1022: Type or namespace definition, or end-of-file expected"),
new ClassifiedText(ClassificationType.BuildResultFailed, "========== Build: 1 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========\r\n")
new ClassifiedText(ClassificationType.BuildActionStartedError, "------ Build started: Project: ConsoleDemo, Configuration: Debug Any CPU ------"),
new ClassifiedText(ClassificationType.BuildResultFailed, "========== Build: 1 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========\r\n"),
new ClassifiedText(ClassificationType.BuildActionStartedSuccess, "------ Build started: Project: WebDemo, Configuration: Debug Any CPU ------")
};
}
}
2 changes: 2 additions & 0 deletions Tests/IntegrationTests/TestCases/PublishResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ public class PublishResult : ITestCase
public IReadOnlyList<ClassifiedText> ExpectedResult { get; } = new[]
{
new ClassifiedText(ClassificationType.BuildResultSucceeded, "========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========\r\n"),
new ClassifiedText(ClassificationType.BuildActionStartedSuccess,"------ Build started: Project: WebDemo, Configuration: Release Any CPU ------"),
new ClassifiedText(ClassificationType.PublishResultSucceeded, "========== Publish: 1 succeeded, 0 failed, 0 skipped ==========\r\n"),
new ClassifiedText(ClassificationType.BuildActionStartedSuccess,"------ Publish started: Project: WebDemo, Configuration: Release Any CPU ------")
};
}
}
3 changes: 3 additions & 0 deletions Tests/UnitTests/ClassificationFormatTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public class ClassificationFormatTests
{ typeof(BuildMessageWarningFormatDefinition), "Output enhancer: Build warning message" },
{ typeof(BuildResultFailedFormatDefinition), "Output enhancer: Build failed" },
{ typeof(BuildResultSucceededFormatDefinition), "Output enhancer: Build succeeded" },
{ typeof(BuildActionStartedSuccessFormatDefinition), "Output enhancer: Build action started and succeeded" },
{ typeof(BuildActionStartedWarningFormatDefinition), "Output enhancer: Build action started and finished with warnings" },
{ typeof(BuildActionStartedErrorFormatDefinition), "Output enhancer: Build action started and failed" },
{ typeof(PublishResultSucceededFormatDefinition), "Output enhancer: Publish succeeded" },
{ typeof(PublishResultFailedFormatDefinition), "Output enhancer: Publish failed" },
{ typeof(DebugExceptionFormatDefinition), "Output enhancer: Debug exception message" },
Expand Down
71 changes: 71 additions & 0 deletions Tests/UnitTests/DispatcherTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using FluentAssertions;
using Xunit;

namespace Balakin.VSOutputEnhancer.Tests.UnitTests
{
[ExcludeFromCodeCoverage]
public class DispatcherTests
{
private class TestEvent : IEvent
{
}

private class AnotherTestEvent : IEvent
{
}

private class TestEventHandler<TEvent> : IEventHandler<TEvent> where TEvent : IEvent
{
public IEnumerable<String> ContentTypes { get; }
public Int32 InvocationCount { get; private set; }

public void Handle(IDispatcher dispatcher, DataContainer data, TEvent @event)
{
InvocationCount++;
}
}

[Fact]
public void AddHandlerDoesNotThrowException()
{
var dispatcher = new Dispatcher();
var handler = new TestEventHandler<TestEvent>();
dispatcher.AddHandler(handler);
}

[Fact]
public void DispatchInvokesHandler()
{
var dispatcher = new Dispatcher();
var handler = new TestEventHandler<TestEvent>();
dispatcher.AddHandler(handler);

var @event = new TestEvent();
var data = new DataContainer();
dispatcher.Dispatch(@event, data);

handler.InvocationCount.Should().Be(1);
}

[Fact]
public void DispatchInvokesOnlyAppropriateHandler()
{
var dispatcher = new Dispatcher();
var handler = new TestEventHandler<TestEvent>();
dispatcher.AddHandler(handler);

var anotherHandler = new TestEventHandler<AnotherTestEvent>();
dispatcher.AddHandler(anotherHandler);

var @event = new TestEvent();
var data = new DataContainer();
dispatcher.Dispatch(@event, data);

handler.InvocationCount.Should().Be(1);
anotherHandler.InvocationCount.Should().Be(0);
}
}
}
3 changes: 3 additions & 0 deletions Tests/UnitTests/StyleManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public void SimilarClassificationTypesHaveSimilarColors(Theme theme)
{
ClassificationType.BuildMessageError,
ClassificationType.BuildResultFailed,
ClassificationType.BuildActionStartedError,
ClassificationType.PublishResultFailed,
ClassificationType.DebugTraceError,
ClassificationType.DebugException,
Expand All @@ -44,13 +45,15 @@ public void SimilarClassificationTypesHaveSimilarColors(Theme theme)
};
var warning = new[]
{
ClassificationType.BuildActionStartedWarning,
ClassificationType.BuildMessageWarning,
ClassificationType.DebugTraceWarning,
ClassificationType.NpmMessageWarning
};
var success = new[]
{
ClassificationType.BuildResultSucceeded,
ClassificationType.BuildActionStartedSuccess,
ClassificationType.PublishResultSucceeded,
ClassificationType.NpmResultSucceeded
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.ComponentModel.Composition;
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
Expand All @@ -14,20 +16,51 @@ public class ClassificationTypeExportsTests
{
[Fact]
public void AllClassificationTypeDefinitionsAreExported()
{
var exportedDefinitions = EnumerateClassificationTypes();

var allDefinitions = ClassificationType.All;
var notExportedDefinitions = allDefinitions.Except(exportedDefinitions);
notExportedDefinitions.Should().BeEmpty("All classification types should be exported");
}

[Theory]
[MemberData(nameof(ClassificationTypesTheoryData))]
public void AllClassificationTypesHaveFormatDefinitionExported(String classificationType)
{
var formatDefinitionType = typeof(ClassificationFormatDefinition);
var assembly = typeof(ClassificationType).Assembly;
var allFormatDefinitions = assembly.GetTypes().Where(formatDefinitionType.IsAssignableFrom);

allFormatDefinitions.Should().ContainSingle(t => t.GetCustomAttributes<ClassificationTypeAttribute>().Any(a => a.ClassificationTypeNames.Contains(classificationType)));
}

public static IEnumerable<Object[]> ClassificationTypesTheoryData()
{
var classificationTypesWithoutFormat = new[] { ClassificationType.DebugTraceInformation };

return EnumerateClassificationTypes()
.Where(t => !classificationTypesWithoutFormat.Contains(t))
.Select(t => new Object[] { t });
}

private static IEnumerable<String> EnumerateClassificationTypes()
{
var exportAttribute = typeof(ExportAttribute);
var classificationTypeDefinition = typeof(ClassificationTypeDefinition);

var allFields = typeof(ClassificationTypeExports).GetFields(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
var exportedDefinitions = allFields
.SelectMany(t => t.GetFields(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public))
.Where(f => f.CustomAttributes.Any(a => a.AttributeType == exportAttribute))
.Where(f => f.GetCustomAttributes<ExportAttribute>().Any(a => a.ContractType == classificationTypeDefinition))
.SelectMany(f => f.GetCustomAttributes<NameAttribute>())
.Select(a => a.Name)
.Distinct();
.Distinct()
.ToList();

var allDefinitions = ClassificationType.All;
var notExportedDefinitions = allDefinitions.Except(exportedDefinitions);
var notExportedDefinitions = allDefinitions.Except(exportedDefinitions).ToList();
notExportedDefinitions.Should().BeEmpty("All classification types should be exported");
}
}
Expand Down
7 changes: 7 additions & 0 deletions VSOutputEnhancer.Logic/ClassificationType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ public static class ClassificationType
BuildMessageWarning,
BuildResultFailed,
BuildResultSucceeded,
BuildActionStartedSuccess,
BuildActionStartedWarning,
BuildActionStartedError,
PublishResultFailed,
PublishResultSucceeded,
DebugTraceError,
Expand All @@ -29,6 +32,10 @@ public static class ClassificationType
public const String BuildResultFailed = "BuildResultFailed";
public const String BuildResultSucceeded = "BuildResultSucceeded";

public const String BuildActionStartedSuccess = "BuildActionStartedSuccess";
public const String BuildActionStartedWarning = "BuildActionStartedWarning";
public const String BuildActionStartedError = "BuildActionStartedError";

public const String PublishResultFailed = "PublishResultFailed";
public const String PublishResultSucceeded = "PublishResultSucceeded";

Expand Down
12 changes: 12 additions & 0 deletions VSOutputEnhancer.Logic/ClassificationTypeExports.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ public static class ClassificationTypeExports
[Name(ClassificationType.BuildResultFailed)]
public static ClassificationTypeDefinition BuildResultFailed;

[Export(typeof(ClassificationTypeDefinition))]
[Name(ClassificationType.BuildActionStartedSuccess)]
public static ClassificationTypeDefinition BuildActionStartedSuccess;

[Export(typeof(ClassificationTypeDefinition))]
[Name(ClassificationType.BuildActionStartedWarning)]
public static ClassificationTypeDefinition BuildActionStartedWarning;

[Export(typeof(ClassificationTypeDefinition))]
[Name(ClassificationType.BuildActionStartedError)]
public static ClassificationTypeDefinition BuildActionStartedError;

[Export(typeof(ClassificationTypeDefinition))]
[Name(ClassificationType.BuildMessageError)]
public static ClassificationTypeDefinition BuildMessageError;
Expand Down
23 changes: 16 additions & 7 deletions VSOutputEnhancer.Logic/Classifier.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Balakin.VSOutputEnhancer.Parsers;
using Balakin.VSOutputEnhancer.Events;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;

namespace Balakin.VSOutputEnhancer.Logic
{
public class Classifier : IClassifier
public class Classifier : IClassifier, IEventHandler<ClassificationChangedEvent>
{
private readonly IDispatcher dispatcher;
private readonly DataContainer dataContainer;
private readonly IReadOnlyCollection<ISpanClassifier> spanClassifiers;
private readonly IClassificationTypeService classificationTypeService;

Expand All @@ -20,32 +23,38 @@ public Classifier(
this.dispatcher = dispatcher;
this.spanClassifiers = spanClassifiers;
this.classificationTypeService = classificationTypeService;

dataContainer = new DataContainer();
}

public IEnumerable<String> ContentTypes => throw new NotSupportedException();

public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span)
{
var result = new List<ClassificationSpan>();
foreach (var classifier in spanClassifiers)
{
var classifierResult = classifier.Classify(span, dispatcher);
var classifierResult = classifier.Classify(span, dispatcher, dataContainer);
var classificationSpans = classifierResult.Select(r => CreateClassificationSpan(span, r));
result.AddRange(classificationSpans);
}

return result;
}

public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged
{
add { }
remove { }
}
public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged;

private ClassificationSpan CreateClassificationSpan(SnapshotSpan originalSpan, ProcessedParsedData data)
{
var classificationType = classificationTypeService.GetClassificationType(data.ClassificationName);
var span = new SnapshotSpan(originalSpan.Snapshot, data.Span);
return new ClassificationSpan(span, classificationType);
}

public void Handle(IDispatcher dispatcher, DataContainer data, ClassificationChangedEvent @event)
{
var eventArgs = new ClassificationChangedEventArgs(@event.Span);
ClassificationChanged?.Invoke(this, eventArgs);
}
}
}
1 change: 1 addition & 0 deletions VSOutputEnhancer.Logic/ClassifierProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public IClassifier GetClassifier(ITextBuffer textBuffer)

var dispatcher = CreateDispatcher(contentType);
var classifier = new Classifier(dispatcher, classifiers, classificationTypeService);
dispatcher.AddHandler(classifier);
return classifier;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public BowerMessageClassifier(IParser<BowerMessageData> parser) : base(parser)
ContentType.Output
};

protected override IEnumerable<ProcessedParsedData> Classify(SnapshotSpan span, BowerMessageData parsedData)
protected override IEnumerable<ProcessedParsedData> Classify(SnapshotSpan span, BowerMessageData parsedData, DataContainer data)
{
var classificationType = GetClassificationType(parsedData.Type);
if (String.IsNullOrEmpty(classificationType))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public BuildFileRelatedMessageClassifier(IParser<BuildFileRelatedMessageData> pa
ContentType.BuildOrderOutput
};

protected override IEnumerable<ProcessedParsedData> Classify(SnapshotSpan span, BuildFileRelatedMessageData parsedData)
protected override IEnumerable<ProcessedParsedData> Classify(SnapshotSpan span, BuildFileRelatedMessageData parsedData, DataContainer data)
{
var messageSpan = parsedData.FullMessage.Span;
if (parsedData.Type == MessageType.Warning)
Expand Down
Loading