diff --git a/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs b/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs index 7ca94153d09..f4644d4e358 100644 --- a/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs @@ -1029,6 +1029,25 @@ public void TestTaskResolutionFailureWithNoUsingTask() _logger.AssertLogContains("MSB4036"); } + /// + /// https://github.com/dotnet/msbuild/issues/8864 + /// + [Fact] + public void TestTaskDictionaryOutputItems() + { + string customTaskPath = Assembly.GetExecutingAssembly().Location; + MockLogger ml = ObjectModelHelpers.BuildProjectExpectSuccess($""" + + + + + + + + + """); + ml.AssertLogContains("a=b"); + } #endregion #region ITestTaskHost Members @@ -1423,11 +1442,11 @@ private ProjectInstance CreateTestProject() - - - - - + + + + + diff --git a/src/Build.UnitTests/BackEnd/TaskThatReturnsDictionaryTaskItem.cs b/src/Build.UnitTests/BackEnd/TaskThatReturnsDictionaryTaskItem.cs new file mode 100644 index 00000000000..c258beb89a4 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/TaskThatReturnsDictionaryTaskItem.cs @@ -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; +/// +/// Task that returns a custom ITaskItem implementation that has a custom IDictionary type returned from CloneCustomMetadata() +/// +public sealed class TaskThatReturnsDictionaryTaskItem : Utilities.Task +{ + public string Key { get; set; } + public string Value { get; set; } + + public override bool Execute() + { + var metaValue = new MinimalDictionary + { + { Key, Value } + }; + DictionaryTaskItemOutput = new MinimalDictionaryTaskItem(metaValue); + return true; + } + + [Output] + public ITaskItem DictionaryTaskItemOutput { get; set; } + + internal sealed class MinimalDictionaryTaskItem : ITaskItem + { + private MinimalDictionary _metaData = new MinimalDictionary(); + + public MinimalDictionaryTaskItem(MinimalDictionary 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 : IDictionary +{ + private List _keys = new List(); + private List _values = new List(); + + 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(_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(_keys[i], _values[i]); + } + } + + private sealed class MinimalDictionaryEnumerator : IDictionaryEnumerator + { + private List _keys; + private List _values; + private int _index = -1; + + public MinimalDictionaryEnumerator(List keys, List 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; + } + } +}