Skip to content
Merged
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
29 changes: 24 additions & 5 deletions src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,25 @@ public void TestTaskResolutionFailureWithNoUsingTask()
_logger.AssertLogContains("MSB4036");
}

/// <summary>
/// https://github.com/dotnet/msbuild/issues/8864
/// </summary>
[Fact]
public void TestTaskDictionaryOutputItems()
{
string customTaskPath = Assembly.GetExecutingAssembly().Location;
MockLogger ml = ObjectModelHelpers.BuildProjectExpectSuccess($"""
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<UsingTask TaskName=`TaskThatReturnsDictionaryTaskItem` AssemblyFile=`{customTaskPath}`/>
<Target Name=`Build`>
<TaskThatReturnsDictionaryTaskItem Key="a" Value="b">
<Output TaskParameter="DictionaryTaskItemOutput" ItemName="Outputs"/>
</TaskThatReturnsDictionaryTaskItem>
</Target>
</Project>
""");
ml.AssertLogContains("a=b");
}
#endregion

#region ITestTaskHost Members
Expand Down Expand Up @@ -1423,11 +1442,11 @@ private ProjectInstance CreateTestProject()
<Target Name='Skip' Inputs='testProject.proj' Outputs='testProject.proj' />

<Target Name='Error' >
<ErrorTask1 ContinueOnError='True'/>
<ErrorTask2 ContinueOnError='False'/>
<ErrorTask3 />
<OnError ExecuteTargets='Foo'/>
<OnError ExecuteTargets='Bar'/>
<ErrorTask1 ContinueOnError='True'/>
<ErrorTask2 ContinueOnError='False'/>
<ErrorTask3 />
<OnError ExecuteTargets='Foo'/>
<OnError ExecuteTargets='Bar'/>
</Target>

<Target Name='Foo' Inputs='foo.cpp' Outputs='foo.o'>
Expand Down
210 changes: 210 additions & 0 deletions src/Build.UnitTests/BackEnd/TaskThatReturnsDictionaryTaskItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// 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;
using System.Collections.Generic;
using Microsoft.Build.Framework;

#nullable disable

namespace Microsoft.Build.Engine.UnitTests;
/// <summary>
/// Task that returns a custom ITaskItem implementation that has a custom IDictionary type returned from CloneCustomMetadata()
/// </summary>
public sealed class TaskThatReturnsDictionaryTaskItem : Utilities.Task
{
public string Key { get; set; }
public string Value { get; set; }

public override bool Execute()
{
var metaValue = new MinimalDictionary<string, string>
{
{ Key, Value }
};
DictionaryTaskItemOutput = new MinimalDictionaryTaskItem(metaValue);
return true;
}

[Output]
public ITaskItem DictionaryTaskItemOutput { get; set; }

internal sealed class MinimalDictionaryTaskItem : ITaskItem
{
private MinimalDictionary<string, string> _metaData = new MinimalDictionary<string, string>();

public MinimalDictionaryTaskItem(MinimalDictionary<string, string> metaValue)
{
_metaData = metaValue;
}

public string ItemSpec { get => $"{nameof(MinimalDictionaryTaskItem)}spec"; set => throw new NotImplementedException(); }

public ICollection MetadataNames => throw new NotImplementedException();

public int MetadataCount => throw new NotImplementedException();

ICollection ITaskItem.MetadataNames => throw new NotImplementedException();

public IDictionary CloneCustomMetadata() => _metaData;

public string GetMetadata(string metadataName)
{
if (String.IsNullOrEmpty(metadataName))
{
throw new ArgumentNullException(nameof(metadataName));
}

string value = (string)_metaData[metadataName];
return value;
}

public void SetMetadata(string metadataName, string metadataValue) => throw new NotImplementedException();
public void RemoveMetadata(string metadataName) => throw new NotImplementedException();
public void CopyMetadataTo(ITaskItem destinationItem) => throw new NotImplementedException();
}
}

public sealed class MinimalDictionary<TKey, TValue> : IDictionary
{
private List<TKey> _keys = new List<TKey>();
private List<TValue> _values = new List<TValue>();

public object this[object key]
{
get
{
int index = _keys.IndexOf((TKey)key);
return index == -1 ? throw new KeyNotFoundException() : (object)_values[index];
}
set
{
int index = _keys.IndexOf((TKey)key);
if (index == -1)
{
_keys.Add((TKey)key);
_values.Add((TValue)value);
}
else
{
_values[index] = (TValue)value;
}
}
}

public bool IsFixedSize => false;

public bool IsReadOnly => false;

public ICollection Keys => _keys;

public ICollection Values => _values;

public int Count => _keys.Count;

public bool IsSynchronized => false;

public object SyncRoot => throw new NotSupportedException();

public void Add(object key, object value)
{
if (_keys.Contains((TKey)key))
{
throw new ArgumentException("An item with the same key has already been added.");
}

_keys.Add((TKey)key);
_values.Add((TValue)value);
}

public void Clear()
{
_keys.Clear();
_values.Clear();
}

public bool Contains(object key)
{
return _keys.Contains((TKey)key);
}

public void CopyTo(Array array, int index)
{
if (array == null)
{
throw new ArgumentNullException(nameof(array));
}

if (array.Rank != 1)
{
throw new ArgumentException("Array must be one-dimensional.", nameof(array));
}

if (index < 0 || index > array.Length)
{
throw new ArgumentOutOfRangeException(nameof(index));
}

if (array.Length - index < Count)
{
throw new ArgumentException("The number of elements in the source is greater than the available space from index to the end of the destination array.");
}

for (int i = 0; i < Count; i++)
{
array.SetValue(new KeyValuePair<TKey, TValue>(_keys[i], _values[i]), index + i);
}
}

public IDictionaryEnumerator GetEnumerator() => new MinimalDictionaryEnumerator(_keys, _values);

public void Remove(object key)
{
int index = _keys.IndexOf((TKey)key);
if (index != -1)
{
_keys.RemoveAt(index);
_values.RemoveAt(index);
}
}

IEnumerator IEnumerable.GetEnumerator()
{
for (int i = 0; i < Count; i++)
{
yield return new KeyValuePair<TKey, TValue>(_keys[i], _values[i]);
}
}

private sealed class MinimalDictionaryEnumerator : IDictionaryEnumerator
{
private List<TKey> _keys;
private List<TValue> _values;
private int _index = -1;

public MinimalDictionaryEnumerator(List<TKey> keys, List<TValue> values)
{
_keys = keys;
_values = values;
}

public object Current => Entry;

public object Key => _keys[_index];

public object Value => _values[_index];

public DictionaryEntry Entry => new DictionaryEntry(Key, Value);

public bool MoveNext()
{
return ++_index < _keys.Count;
}

public void Reset()
{
_index = -1;
}
}
}