-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve and enable writing test wrappers to disk (#83444)
`CodeBuilder` utility (mainly handles indentation) was lifted from the [PROSE codebase](https://msdata.visualstudio.com/PROSE/_git/PROSE?version=GCa244bb231dcaabe04b4825d4e093beaceed434d8&path=/Microsoft.ProgramSynthesis/Common/Utils/CodeBuilder.cs). I omitted the last two revisions to it as they opened a few API questions for me and I didn't need the functionality anyway. Then use it to format the generated code in `XUnitWrapperGenerator`. Diffs are probably best viewed with whitespace ignored. Writing simple text (`Append`/`AppendLine`) is the same as `StringBuilder`. Indentation is specified (usually with a `using` that cleans up) and then added automatically. Interesting tidbits: - Factored `AppendAliasMap` from the 3 identical uses - The loop structure for breaking the tests into groups of 50 doesn't allow `using`. I did the push/pop of indentation manually rather than trying to restructure it. Add a new MergedTestRunner.targets for relevant logic. Enable `EmitCompilerGeneratedFiles` in it.
- Loading branch information
Showing
40 changed files
with
612 additions
and
259 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.IO; | ||
using System.Text; | ||
using static System.FormattableString; | ||
|
||
namespace XUnitWrapperGenerator; | ||
|
||
/// <summary> | ||
/// A helper class for generating indented code. Indentation is automatically added to lines. | ||
/// Trailing whitespace is removed from lines. | ||
/// </summary> | ||
[DebuggerDisplay("Code = {_code}")] | ||
public class CodeBuilder { | ||
private readonly Stack<int> _indentLevels; | ||
private string _currentIndentString; | ||
private readonly int _indentSize; | ||
private readonly StringBuilder _code; | ||
private const int DefaultAdditionalIndent = 1; | ||
|
||
private sealed class IndentationContext : IDisposable { | ||
private CodeBuilder Builder { get; } | ||
private bool _disposed; | ||
private string? EndLine { get; } | ||
|
||
/// <summary> | ||
/// Constructor. | ||
/// </summary> | ||
/// <param name="builder">The <see cref="CodeBuilder"/> associated with this object.</param> | ||
/// <param name="additionalIndent">The number of indentation levels to add.</param> | ||
/// <param name="endLine">Line to add after disposing the indentation context</param> | ||
public IndentationContext(CodeBuilder builder, uint additionalIndent = DefaultAdditionalIndent, string? endLine = null) { | ||
Builder = builder; | ||
_disposed = false; | ||
EndLine = endLine; | ||
Builder.PushIndent(additionalIndent); | ||
} | ||
|
||
/// <summary> | ||
/// Performs cleanup actions at the end of the lifetime. | ||
/// This involves decreasing the level of indentation on | ||
/// the <see cref="CodeBuilder"/> object that was used to | ||
/// construct this <see cref="IndentationContext"/>. | ||
/// </summary> | ||
public void Dispose() { | ||
if (_disposed) return; | ||
Builder.PopIndent(); | ||
if (EndLine != null) Builder.AppendLine(EndLine); | ||
_disposed = true; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Constructor. | ||
/// </summary> | ||
/// <param name="indentSize">The number of spaces each level of indentation adds.</param> | ||
public CodeBuilder(uint indentSize = 4) { | ||
_indentLevels = new Stack<int>(); | ||
_indentSize = Convert.ToInt32(indentSize); | ||
_currentIndentString = ""; | ||
_code = new StringBuilder(); | ||
_indentLevels.Push(0); | ||
} | ||
|
||
public bool IsEmpty => _code.Length == 0; | ||
|
||
public static CodeBuilder Create(string initialCode) { | ||
var code = new CodeBuilder(); | ||
code.Append(initialCode); | ||
return code; | ||
} | ||
|
||
public static CodeBuilder CreateNewLine(string initialCode) { | ||
var code = new CodeBuilder(); | ||
code.Append(initialCode); | ||
code.AppendLine(); | ||
return code; | ||
} | ||
|
||
/// <summary> | ||
/// Push a new indent level. | ||
/// </summary> | ||
/// <param name="additionalIndent">The amount of indentation to add.</param> | ||
public void PushIndent(uint additionalIndent = DefaultAdditionalIndent) { | ||
int existingIndent = _indentLevels.Peek(); | ||
var newIndent = (int) (existingIndent + additionalIndent); | ||
_indentLevels.Push(newIndent); | ||
_currentIndentString = new string(' ', newIndent * _indentSize); | ||
} | ||
|
||
/// <summary> | ||
/// Pop an indent level (and restore the indent level to before the last call to <see cref="PushIndent" />). | ||
/// </summary> | ||
public void PopIndent() { | ||
_indentLevels.Pop(); | ||
_currentIndentString = new string(' ', _indentLevels.Peek() * _indentSize); | ||
} | ||
|
||
private bool AtStartOfLine() { | ||
if (_code.Length == 0) { | ||
return true; | ||
} | ||
|
||
return _code[_code.Length - 1] == '\n'; | ||
} | ||
|
||
private void Append(string code, bool allowLeadingWhiteSpace) { | ||
if (string.IsNullOrEmpty(code)) return; | ||
|
||
string[] lines = code.Split('\n'); | ||
|
||
// Do entire check first to avoid a partial write in the case of failure | ||
if (!allowLeadingWhiteSpace) { | ||
for (int i = 0; i < lines.Length; ++i) { | ||
if ((i > 0 || AtStartOfLine()) | ||
&& (lines[i].Length > 0) && char.IsWhiteSpace(lines[i][0])) { | ||
throw new ArgumentException(Invariant($@"Whitespace (0x{(int)lines[i][0]:x2}) at start of line {i} in input '{code}'")); | ||
} | ||
} | ||
} | ||
|
||
for (int i = 0; i < lines.Length; ++i) { | ||
if (i != 0) AppendLine(); | ||
|
||
string line = lines[i]; | ||
if (AtStartOfLine() && !string.IsNullOrWhiteSpace(line)) _code.Append(_currentIndentString); | ||
_code.Append(line); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Append the given code. The currently active indentation level is applied at newlines. | ||
/// </summary> | ||
/// <exception cref="System.ArgumentException">Thrown when a line already contains leading whitespace</exception> | ||
/// <param name="code">The code to append.</param> | ||
public void Append(string code) => Append(code, allowLeadingWhiteSpace: false); | ||
|
||
/// <summary> | ||
/// Append the given, already-indented code. The currently active indentation level is also applied at newlines. | ||
/// </summary> | ||
/// <param name="code">The code to append.</param> | ||
public void AppendIndented(string code) => Append(code, allowLeadingWhiteSpace: true); | ||
|
||
/// <summary> | ||
/// Append the given, already-indented code. The currently active indentation level is also applied at newlines. | ||
/// </summary> | ||
/// <param name="code">The code to append.</param> | ||
public void Append(CodeBuilder code) => AppendIndented(code.GetCode()); | ||
|
||
/// <summary> | ||
/// Append the given code followed by a line terminator. The currently active indentation level is applied at newlines. | ||
/// </summary> | ||
/// <param name="codeLine">The line to append.</param> | ||
public void AppendLine(string codeLine) { | ||
Append(codeLine); | ||
AppendLine(); | ||
} | ||
|
||
/// <summary>Append a blank line.</summary> | ||
public void AppendLine() { | ||
int lastToKeep; | ||
for (lastToKeep = _code.Length - 1; lastToKeep >= 0; --lastToKeep) { | ||
if (_code[lastToKeep] == '\n' || !char.IsWhiteSpace(_code[lastToKeep])) { | ||
break; | ||
} | ||
} | ||
_code.Length = lastToKeep + 1; | ||
_code.AppendLine(); | ||
} | ||
|
||
/// <summary> | ||
/// Appends a block of code using the current indentation. | ||
/// </summary> | ||
/// <param name="block">The code block.</param> | ||
public void AppendBlock(string block) { | ||
if (block == null) throw new ArgumentNullException(nameof(block)); | ||
|
||
using (var reader = new StringReader(block)) { | ||
string line = reader.ReadLine(); | ||
if (line == null) return; | ||
|
||
AppendIndented(line); | ||
while ((line = reader.ReadLine()) != null) { | ||
AppendLine(); | ||
AppendIndented(line); | ||
} | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Creates a new <see cref="IndentationContext"/>. | ||
/// </summary> | ||
/// <param name="introduction"> | ||
/// String to add before the braces and indentation context. If non-empty, then a new line will be | ||
/// appended after it. | ||
/// </param> | ||
/// <param name="additionalIndent">Number of indentation levels to add.</param> | ||
/// <returns>A new <see cref="IndentationContext"/>.</returns> | ||
public IDisposable NewScope(string? introduction = null, uint additionalIndent = DefaultAdditionalIndent) { | ||
if (!string.IsNullOrEmpty(introduction)) { | ||
this.AppendLine(introduction!); | ||
} | ||
return new IndentationContext(this, additionalIndent); | ||
} | ||
|
||
/// <summary> | ||
/// Creates a new <see cref="IndentationContext"/> with an introduction and | ||
/// surrounded by braces. | ||
/// </summary> | ||
/// <param name="introduction">String to add before the braces and indentation context.</param> | ||
/// <param name="additionalIndent">Number of indentation levels to add.</param> | ||
/// <returns>A new <see cref="IndentationContext"/>.</returns> | ||
public IDisposable NewBracesScope(string? introduction = null, uint additionalIndent = DefaultAdditionalIndent) { | ||
if (!string.IsNullOrEmpty(introduction)) { | ||
this.Append(introduction!); | ||
} | ||
this.AppendLine(string.IsNullOrEmpty(introduction) ? "{" : " {"); | ||
return new IndentationContext(this, additionalIndent: additionalIndent, endLine: "}"); | ||
} | ||
|
||
/// <summary>Returns the built-up code.</summary> | ||
/// <returns>The built-up code.</returns> | ||
public string GetCode() => _code.ToString(); | ||
} |
Oops, something went wrong.