From b1583b595e57687fd167d496ce44ae0c04e35c06 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Fri, 22 Nov 2024 12:41:22 -0800 Subject: [PATCH] Support Validation Warnings (#54) --- CHANGELOG.md | 11 +++ Editor/Common/Util/ParameterDebug.cs | 6 ++ .../Operation/DataOperationContext.cs | 4 + .../Operation/IDataOperationContext.cs | 6 ++ .../Operations/DataValidationOperation.cs | 14 ++- .../ParameterScriptableObjectInspector.cs | 73 ++++++++++++--- Editor/Editor/SerializableDateTimeDrawer.cs | 2 +- Editor/Editor/SerializableTimeSpanDrawer.cs | 2 +- .../Editor/Validation/ValidationTreeView.cs | 8 +- Editor/Editor/Validation/ValidationWindow.cs | 93 ++++++++++++++++++- Editor/EditorParameterDataManager.cs | 40 ++++++-- README/DataValidation.md | 20 ++-- Runtime/Validation/BaseDataValidator.cs | 23 +++++ .../Validation/ParameterManagerValidator.cs | 33 ++++++- Runtime/Validation/ValidationError.cs | 13 ++- .../Operation/CodeOperationContextTest.cs | 1 + .../Editor/Common/Util/ParameterDebugTest.cs | 7 ++ .../Operation/DataOperationContextTest.cs | 1 + .../Runtime/TestCode/TestBaseDataValidator.cs | 8 +- .../Validation/BaseDataValidatorTest.cs | 22 ++++- package.json | 2 +- 21 files changed, 338 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c90300f..c366dc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All package updates & migration steps will be listed in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.3.0] - 2024-11-22 +### Added +- `Warning` and `Error` severity levels for `ValidatorError` +- Updated UI to display and filter warnings & errors. +- Some missing coverage. +- Built-in warnings for slow validation execution. +### Changed +- Only mark data generation as an error for `ValidaitonError` with severity error. +- Missing validators are considered warnings. This reduces the noisiness of compilation errors causing this. +- Do not force open the Validation window for warnings. + ## [4.2.1] - 2024-11-19 ### Fixed - Issue where nested structs in scriptable object drawers have glitching UI. diff --git a/Editor/Common/Util/ParameterDebug.cs b/Editor/Common/Util/ParameterDebug.cs index 9a7dc18..b2f0d60 100644 --- a/Editor/Common/Util/ParameterDebug.cs +++ b/Editor/Common/Util/ParameterDebug.cs @@ -22,6 +22,12 @@ public static void LogVerbose(string log) /// String to log public static void LogError(string log) => Debug.LogError(log); + /// + /// Warning logs to the console. + /// + /// String to log + public static void LogWarning(string log) => Debug.LogWarning(log); + /// /// Logs to the console. /// diff --git a/Editor/DataGeneration/Operation/DataOperationContext.cs b/Editor/DataGeneration/Operation/DataOperationContext.cs index 592ead1..be1f4e3 100644 --- a/Editor/DataGeneration/Operation/DataOperationContext.cs +++ b/Editor/DataGeneration/Operation/DataOperationContext.cs @@ -3,6 +3,7 @@ using PocketGems.Parameters.Common.Models.Editor; using PocketGems.Parameters.Common.Operation.Editor; using PocketGems.Parameters.DataGeneration.LocalCSV.Editor; +using PocketGems.Parameters.Validation; namespace PocketGems.Parameters.DataGeneration.Operation.Editor { @@ -14,6 +15,7 @@ public DataOperationContext() InfoCSVFileCache = new InfoCSVFileCache(GeneratedLocalCSVDirectory, true); StructCSVFileCache = new StructCSVFileCache(GeneratedLocalCSVDirectory, true); ScriptableObjectMetadatas = new Dictionary>(); + AllValidationErrors = new(); } public GenerateDataType GenerateDataType { get; set; } @@ -36,5 +38,7 @@ public DataOperationContext() #endif public Dictionary> ScriptableObjectMetadatas { get; } public List GeneratedFilePaths { get; } + + public List AllValidationErrors { get; } } } diff --git a/Editor/DataGeneration/Operation/IDataOperationContext.cs b/Editor/DataGeneration/Operation/IDataOperationContext.cs index a393f12..36a9247 100644 --- a/Editor/DataGeneration/Operation/IDataOperationContext.cs +++ b/Editor/DataGeneration/Operation/IDataOperationContext.cs @@ -2,6 +2,7 @@ using PocketGems.Parameters.Common.Models.Editor; using PocketGems.Parameters.Common.Operation.Editor; using PocketGems.Parameters.DataGeneration.LocalCSV.Editor; +using PocketGems.Parameters.Validation; namespace PocketGems.Parameters.DataGeneration.Operation.Editor { @@ -41,5 +42,10 @@ public interface IDataOperationContext : ICommonOperationContext /// Asset files that were generated through this data generation /// List GeneratedFilePaths { get; } + + /// + /// Validation errors & warnings + /// + List AllValidationErrors { get; } } } diff --git a/Editor/DataGeneration/Operations/DataValidationOperation.cs b/Editor/DataGeneration/Operations/DataValidationOperation.cs index 80fff4e..d948ca0 100644 --- a/Editor/DataGeneration/Operations/DataValidationOperation.cs +++ b/Editor/DataGeneration/Operations/DataValidationOperation.cs @@ -25,13 +25,23 @@ public override void Execute(IDataOperationContext context) // validate assets var assetErrors = AssetValidator.ValidateScriptableObjects(context.ScriptableObjectMetadatas); for (int i = 0; i < assetErrors?.Count; i++) - Error(assetErrors[i]); + { + var validationError = assetErrors[i]; + context.AllValidationErrors.Add(validationError); + if (validationError.ErrorSeverity == ValidationError.Severity.Error) + Error(assetErrors[i]); + } // validate parameters IParameterManager parameterManager = EditorParams.ParameterManager; var parameterErrors = InvokeParamsValidation(context, parameterManager); for (int i = 0; i < parameterErrors?.Count; i++) - Error(parameterErrors[i]); + { + var validationError = parameterErrors[i]; + context.AllValidationErrors.Add(validationError); + if (validationError.ErrorSeverity == ValidationError.Severity.Error) + Error(parameterErrors[i]); + } } /// diff --git a/Editor/Editor/ParameterScriptableObjectInspector.cs b/Editor/Editor/ParameterScriptableObjectInspector.cs index f6182e6..2631d48 100644 --- a/Editor/Editor/ParameterScriptableObjectInspector.cs +++ b/Editor/Editor/ParameterScriptableObjectInspector.cs @@ -85,17 +85,38 @@ protected void DrawParameterToolGUI() EditorGUILayout.Space(); } + #region hack for backwards compatibility + + private static string warningMessageHolder = null; + + [Obsolete] + private void InternalDrawProperty(SerializedProperty property, string errorMessage = null, string warningMessage = null) + { + warningMessageHolder = warningMessage; + DrawProperty(property, errorMessage); + } + + [Obsolete] + protected virtual void DrawProperty(SerializedProperty property, string errorMessage = null) + { + DrawProperty(property, errorMessage, warningMessageHolder); + } + + #endregion + /// /// Allow for subclasses to draw custom visualizers for the property /// /// property to render /// error message to show if any - protected virtual void DrawProperty(SerializedProperty property, string errorMessage = null) + protected virtual void DrawProperty(SerializedProperty property, string errorMessage = null, string warningMessage = null) { var propLabel = new GUIContent(property.displayName); EditorGUILayout.PropertyField(property, propLabel, true); if (errorMessage != null) EditorGUILayout.HelpBox(errorMessage, MessageType.Error); + if (warningMessage != null) + EditorGUILayout.HelpBox(warningMessage, MessageType.Warning); } public override void OnInspectorGUI() @@ -147,8 +168,8 @@ public override void OnInspectorGUI() errors.Add(error); } - // collect properties & errors - List<(SerializedProperty, string)> properties = new(); + // collect properties, errors & warnings + List<(SerializedProperty, string, string)> properties = new(); // call to ensure the values are up to date with the target in case it was modified in another inspector/drawer serializedObject.Update(); var serializedProp = serializedObject.GetIterator(); @@ -159,17 +180,28 @@ public override void OnInspectorGUI() // don't draw class file if (serializedProp.name == "m_Script") continue; var interfacePropertyGetterName = GetPropertyGetterName(serializedProp); - string errorMessage = null; + StringBuilder errorMessage = null; + StringBuilder warningMessage = null; if (_propertyToError.ContainsKey(interfacePropertyGetterName)) { - StringBuilder s = new StringBuilder(); - s.Append($"{interfacePropertyGetterName} Validation Error(s):"); var propertyErrors = _propertyToError[interfacePropertyGetterName]; _propertyToError.Remove(interfacePropertyGetterName); for (int i = 0; i < propertyErrors.Count; i++) { + StringBuilder s; var error = propertyErrors[i]; - s.AppendLine(); + if (error.ErrorSeverity == ValidationError.Severity.Error) + { + errorMessage ??= new(); + s = errorMessage; + } + else + { + warningMessage ??= new(); + s = warningMessage; + } + if (s.Length != 0) + s.AppendLine(); s.Append(" "); if (!string.IsNullOrEmpty(error.StructKeyPath)) { @@ -190,21 +222,32 @@ public override void OnInspectorGUI() } s.Append($"{error.Message}"); } - - errorMessage = s.ToString(); } - properties.Add((serializedProp.Copy(), errorMessage)); + properties.Add((serializedProp.Copy(), errorMessage?.ToString(), warningMessage?.ToString())); } // display missing maps (shouldn't ever happen but just in case...) if (_propertyToError.Count != 0) { - StringBuilder s = new StringBuilder("Unexpected Errors:"); + StringBuilder errorMessage = null; + StringBuilder warningMessage = null; foreach (var kvp in _propertyToError) { var errors = kvp.Value; for (int i = 0; i < errors.Count; i++) { + StringBuilder s; + var error = errors[i]; + if (error.ErrorSeverity == ValidationError.Severity.Error) + { + errorMessage ??= new("Unexpected Error(s):"); + s = errorMessage; + } + else + { + warningMessage ??= new("Unexpected Warning(s):"); + s = warningMessage; + } s.AppendLine(); if (!string.IsNullOrEmpty(errors[i].InfoProperty)) s.Append($"{errors[i].InfoProperty}: "); @@ -212,7 +255,10 @@ public override void OnInspectorGUI() } } EditorGUILayout.Space(); - EditorGUILayout.HelpBox(s.ToString(), MessageType.Error); + if (errorMessage != null) + EditorGUILayout.HelpBox(errorMessage.ToString(), MessageType.Error); + if (warningMessage != null) + EditorGUILayout.HelpBox(warningMessage.ToString(), MessageType.Warning); EditorGUILayout.Space(); } @@ -227,6 +273,7 @@ public override void OnInspectorGUI() { serializedProp = properties[i].Item1; var errorMessage = properties[i].Item2; + var warningMessage = properties[i].Item3; // don't draw class file if (serializedProp.name == "m_Script") continue; @@ -255,7 +302,7 @@ public override void OnInspectorGUI() // draw property if (foldOutAttribute == null || isFoldoutOpen) - DrawProperty(serializedProp, errorMessage); + InternalDrawProperty(serializedProp, errorMessage, warningMessage); } scope?.Dispose(); diff --git a/Editor/Editor/SerializableDateTimeDrawer.cs b/Editor/Editor/SerializableDateTimeDrawer.cs index 1fa2e7c..9013f56 100644 --- a/Editor/Editor/SerializableDateTimeDrawer.cs +++ b/Editor/Editor/SerializableDateTimeDrawer.cs @@ -58,7 +58,7 @@ int DrawComponent(string componentLabel, int componentDigits, int componentValue var second = DrawComponent("s", 2, dateTime.Second); dateTime = new DateTime(year, month, day, hour, minute, second); } - catch (Exception _) + catch { // ignored } diff --git a/Editor/Editor/SerializableTimeSpanDrawer.cs b/Editor/Editor/SerializableTimeSpanDrawer.cs index c07b43e..9b63d33 100644 --- a/Editor/Editor/SerializableTimeSpanDrawer.cs +++ b/Editor/Editor/SerializableTimeSpanDrawer.cs @@ -57,7 +57,7 @@ float DrawComponent(string componentLabel, int componentDigits, int componentVal newTimeSpan += TimeSpan.FromSeconds(DrawComponent("s", 2, timeSpan.Seconds)); newTimeSpan += TimeSpan.FromMilliseconds(DrawComponent("ms", 3, timeSpan.Milliseconds)); } - catch (Exception e) + catch { // ignored } diff --git a/Editor/Editor/Validation/ValidationTreeView.cs b/Editor/Editor/Validation/ValidationTreeView.cs index 20f5c51..f11de10 100644 --- a/Editor/Editor/Validation/ValidationTreeView.cs +++ b/Editor/Editor/Validation/ValidationTreeView.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using PocketGems.Parameters.Editor.Validation.TreeDataModel.Editor; +using PocketGems.Parameters.Validation; using UnityEditor; using UnityEditor.IMGUI.Controls; using UnityEngine; @@ -134,7 +135,12 @@ void CellGUI(Rect cellRect, TreeViewItem item, Columns co case Columns.Icon: { if (!string.IsNullOrEmpty(item.data.ValidationError?.Message)) - GUI.DrawTexture(cellRect, _errorIcon, ScaleMode.ScaleToFit); + { + var icon = _errorIcon; + if (item.data.ValidationError.ErrorSeverity == ValidationError.Severity.Warning) + icon = _warningIcon; + GUI.DrawTexture(cellRect, icon, ScaleMode.ScaleToFit); + } } break; case Columns.PropertyName: diff --git a/Editor/Editor/Validation/ValidationWindow.cs b/Editor/Editor/Validation/ValidationWindow.cs index c1cd20c..74e61ef 100644 --- a/Editor/Editor/Validation/ValidationWindow.cs +++ b/Editor/Editor/Validation/ValidationWindow.cs @@ -33,6 +33,8 @@ private enum ValidationState // models [NonSerialized] private IReadOnlyList _errors; + [NonSerialized] private int _errorCount; + [NonSerialized] private int _warningCount; [NonSerialized] private ValidationState _validationState = ValidationState.NoData; [NonSerialized] private long _executionMillis; @@ -43,6 +45,8 @@ private enum ValidationState [SerializeField] MultiColumnHeaderState _multiColumnHeaderState; SearchField _searchField; ValidationTreeView _treeView; + private bool _showErrors = true; + private bool _showWarnings = true; // red flickering notifier private bool _disableNextFlicker; @@ -51,6 +55,11 @@ private enum ValidationState private Stopwatch _flickerStopwatch = new Stopwatch(); private const float s_flickerDuration = 0.25f; + // view + private Texture2D _errorIcon; + private Texture2D _warningIcon; + private Texture2D _infoIcon; + [MenuItem(MenuItemConstants.ValidationWindowPath, false, MenuItemConstants.ValidationWindowPriority)] public static void OpenWindow() { @@ -72,6 +81,18 @@ internal void UpdateValidationResult(IReadOnlyList errors, long { _isDirty = true; _errors = errors; + _errorCount = 0; + _warningCount = 0; + if (_errors?.Count > 0) + { + foreach (var error in _errors) + { + if (error.ErrorSeverity == ValidationError.Severity.Error) + _errorCount++; + else + _warningCount++; + } + } _executionMillis = executionMillis; _validationState = errors?.Count > 0 ? ValidationState.NoErrors @@ -113,6 +134,8 @@ private void ClearValidationResults() ValidationWindowSerializer.SerializeToStorage(null); _isDirty = true; _errors = null; + _errorCount = 0; + _warningCount = 0; _validationState = ValidationState.NoData; } @@ -143,6 +166,11 @@ private void InitIfNeeded() _searchField = new SearchField(); _searchField.downOrUpArrowKeyPressed += _treeView.SetFocusAndEnsureSelectedItem; + // found texture examples in AssetSettingsAnalyzeTreeView + _errorIcon = EditorGUIUtility.FindTexture("console.errorIcon"); + _warningIcon = EditorGUIUtility.FindTexture("console.warnicon"); + _infoIcon = EditorGUIUtility.FindTexture("console.infoIcon"); + _isInit = true; } } @@ -214,9 +242,38 @@ private IList CreateTreeElements() var rootElement = new ValidationTreeElement(ValidationTreeElement.TreeElementType.Root, "root", idCount++); elements.Add(rootElement); + List filteredErrors = null; if (_errors != null && _errors.Count > 0) { - List sortedErrors = new List(_errors); + foreach (var error in _errors) + { + if ((_showErrors && error.ErrorSeverity == ValidationError.Severity.Error) || + (_showWarnings && error.ErrorSeverity == ValidationError.Severity.Warning)) + { + if (filteredErrors == null) + filteredErrors = new(); + filteredErrors.Add(error); + } + } + } + + if (!_showWarnings && _warningCount > 0) + { + string status = _warningCount == 1 ? "1 warning is hidden." : $"{_warningCount} warnings are hidden."; + var parentElement = new ValidationTreeElement(ValidationTreeElement.TreeElementType.General, status, idCount++); + elements.Add(parentElement); + } + + if (!_showErrors && _errorCount > 0) + { + string status = _errorCount == 1 ? "1 error is hidden." : $"{_errorCount} errors are hidden."; + var parentElement = new ValidationTreeElement(ValidationTreeElement.TreeElementType.General, status, idCount++); + elements.Add(parentElement); + } + + if (filteredErrors != null && filteredErrors.Count > 0) + { + List sortedErrors = new(filteredErrors); int ErrorCompare(ValidationError a, ValidationError b) { if (a.InfoType != b.InfoType) @@ -273,7 +330,8 @@ int ErrorCompare(ValidationError a, ValidationError b) elements.Add(element); } } - else + + if (_errors == null || _errors.Count == 0) { string status = _validationState == ValidationState.NoData ? "No Validation Results" : "No Errors"; var parentElement = new ValidationTreeElement(ValidationTreeElement.TreeElementType.General, status, idCount++); @@ -324,7 +382,34 @@ private void ToolBar(Rect rect) { ClearValidationResults(); } - var searchRect = new Rect(rect.x + 290, rect.y, rect.width - 290, rect.height); + + // show error & warning toggle on the very right + var errorButton = new GUIContent(_errorCount.ToString(), _errorIcon); + Vector2 errorButtonSize = EditorStyles.toolbarButton.CalcSize(errorButton); + var errorButtonRect = new Rect( + rect.x + rect.width - errorButtonSize.x, + rect.y, + errorButtonSize.x, + errorButtonSize.y); + var newShowErrors = GUI.Toggle(errorButtonRect, _showErrors, errorButton, EditorStyles.toolbarButton); + _isDirty |= newShowErrors != _showErrors; + _showErrors = newShowErrors; + + var warningButton = new GUIContent(_warningCount.ToString(), _warningIcon); + Vector2 warningButtonSize = EditorStyles.toolbarButton.CalcSize(warningButton); + var warningButtonRect = new Rect( + rect.x + rect.width - warningButtonSize.x - errorButtonSize.x, + rect.y, + warningButtonSize.x, + warningButtonSize.y); + var newShowWarnings = GUI.Toggle(warningButtonRect, _showWarnings, warningButton, EditorStyles.toolbarButton); + _isDirty |= newShowWarnings != _showWarnings; + _showWarnings = newShowWarnings; + var searchRect = new Rect( + rect.x + 290, + rect.y, + rect.width - 290 - errorButtonSize.x - warningButtonSize.x - 5, + rect.height); _treeView.searchString = _searchField.OnGUI(searchRect, _treeView.searchString); } @@ -362,8 +447,6 @@ private void BottomToolBar(Rect rect) { _treeView.CollapseAll(); } - if (_validationState != ValidationState.NoData) - GUILayout.Label($"Total Errors: {_errors?.Count ?? 0}"); GUILayout.FlexibleSpace(); if (_validationState != ValidationState.NoData && _executionMillis != 0) GUILayout.Label($"Validation Execution Duration: {_executionMillis}ms"); diff --git a/Editor/EditorParameterDataManager.cs b/Editor/EditorParameterDataManager.cs index 73a4cac..ca170d4 100644 --- a/Editor/EditorParameterDataManager.cs +++ b/Editor/EditorParameterDataManager.cs @@ -452,6 +452,7 @@ public static bool GenerateData(GenerateDataType generateType, * output/display results *************************/ DisplayExecutionResults(executor); + DisplayValidationResults(executor, context); s_runningGenerateData = false; ParameterDebug.LogVerbose($"{nameof(GenerateData)} duration: {generateDataDuration}ms"); @@ -536,32 +537,55 @@ private static void DisplayExecutionResults(OperationExecutor executor) $"There are {errorMessages.Count} parameter errors. See console for error logs.", "Okay"); - if (Application.isBatchMode) { for (int i = 0; i < validationErrors.Count; i++) ParameterDebug.LogError(validationErrors[i].ToString()); } - else if (validationErrors.Count > 0) + } + } + + private static void DisplayValidationResults(OperationExecutor executor, IDataOperationContext context) + { + if (executor.ExecutorState == ExecutorState.Error) + { + bool hasErrorSeverity = false; + foreach (var validationError in context.AllValidationErrors) + { + if (validationError.ErrorSeverity == ValidationError.Severity.Error) + { + hasErrorSeverity = true; + break; + } + } + + if (hasErrorSeverity) { ParameterDebug.LogError("Parameter errors: see Parameter Validation window"); - ValidationWindow.SerializeToStorage(validationErrors); + // only force open the window on validation errors not warnings + ValidationWindow.SerializeToStorage(context.AllValidationErrors); var window = ValidationWindow.GetWindow(true); - window.UpdateValidationResult(validationErrors, executor.ExecuteMilliseconds); + window.UpdateValidationResult(context.AllValidationErrors, executor.ExecuteMilliseconds); } } if (executor.ExecutorState == ExecutorState.Finished) { - if (typeof(T) == typeof(IDataOperationContext) && !executor.ShortCircuited && - ParameterPrefs.AutoValidateDataOnAssetChange) + if (!executor.ShortCircuited && ParameterPrefs.AutoValidateDataOnAssetChange) { - ValidationWindow.SerializeToStorage(null); + if (context.AllValidationErrors.Count > 0) + { + ParameterDebug.LogWarning("Parameter warnings: see Parameter Validation window"); + } + + // update the window with the latest results if it happens to be open + // if it got this far, the only errors there may have are warnings + ValidationWindow.SerializeToStorage(context.AllValidationErrors); if (ValidationWindow.HasOpenInstance()) { var window = ValidationWindow.GetWindow(false); - window.UpdateValidationResult(null, executor.ExecuteMilliseconds); + window.UpdateValidationResult(context.AllValidationErrors, executor.ExecuteMilliseconds); } } } diff --git a/README/DataValidation.md b/README/DataValidation.md index 0750601..1d8c092 100644 --- a/README/DataValidation.md +++ b/README/DataValidation.md @@ -116,21 +116,22 @@ namespace Parameters.Validation ### ValidateInfo `ValidateInfo` will be called for every row of data that conforms to `ICurrencyInfo`. More complex checks that cannot be enforced via attributes can be written here. -Upon finding an error `Error` should be called with the `nameof()` the property and the error message. The system already knows the `Identifier` of the `info` so there is no need to incooporate that as part of the error message. +Upon finding an error or warning `Error` or `Warn` should be called respectively with the `nameof()` the property and the error message. The system already knows the `Identifier` of the `info` so there is no need to incorporate that as part of the error message. ```C# protected override void ValidateInfo(IParameterManager parameterManager, ICurrencyInfo info) { - if (info.IsPremiumCurrency && string.IsNullOrEmpty(info.PremiumCurrencyIconSprite.AssetGUID)) - Error(nameof(ICurrencyInfo.PremiumCurrencyIconSprite), "required for premium currency"); + if (info.IsPremiumCurrency && string.IsNullOrEmpty(info.PremiumCurrencyIconSprite.AssetGUID)) + Error(nameof(ICurrencyInfo.PremiumCurrencyIconSprite), "required for premium currency"); - // add other checks here + if (info.MaxCurrency < 100) + Warn(nameof(ICurrencyInfo.MaxCurrency), "might be too low of a max"); } ``` ### ValidateParameters ValidateParameters will be called once. This can be used for more holistic checks. -Upon error, `Error` can be called with the error message. +Upon error or warning, `Error` or `Warn` can be called respectively with a message. ```C# protected override void ValidateParameters(IParameterManager parameterManager) { @@ -142,7 +143,8 @@ protected override void ValidateParameters(IParameterManager parameterManager) else seenColors.Add(info.CurrencyColor); } - // add other checks here + if (parameterManager.Get().ToList().Count > 1000) + Warn("There may be too many currencies in the game consider reducing"); } ``` @@ -155,9 +157,7 @@ In this validation, the code doesn't know about the context of where the data is ```C# protected override void ValidateStruct(IParameterManager parameterManager, IRewardStruct structObj) { - if (structObj.IsPremiumCurrency && string.IsNullOrEmpty(structObj.PremiumCurrencyIconSprite.AssetGUID)) - Error(nameof(IRewardStruct.PremiumCurrencyIconSprite), "required for premium currency"); - - // add other checks here + if (structObj.IsPremiumCurrency && string.IsNullOrEmpty(structObj.PremiumCurrencyIconSprite.AssetGUID)) + Error(nameof(IRewardStruct.PremiumCurrencyIconSprite), "required for premium currency"); } ``` \ No newline at end of file diff --git a/Runtime/Validation/BaseDataValidator.cs b/Runtime/Validation/BaseDataValidator.cs index 15519c9..444d196 100644 --- a/Runtime/Validation/BaseDataValidator.cs +++ b/Runtime/Validation/BaseDataValidator.cs @@ -50,6 +50,29 @@ protected void Error(string message) _errors.Add(error); } + /// + /// Log a warning during validation. + /// + /// property name with the warning + /// user facing message about the warning + protected void Warn(string propertyName, string message) + { + var error = new ValidationError(typeof(T), _currentIdentifier, propertyName, message, + severity: ValidationError.Severity.Warning); + _errors.Add(error); + } + + /// + /// General warning during validation. + /// + /// user facing message about the warning + protected void Warn(string message) + { + var error = new ValidationError(typeof(T), _currentIdentifier, null, message, + severity: ValidationError.Severity.Warning); + _errors.Add(error); + } + void IDataValidator.ValidateInfo(IParameterManager parameterManager, IBaseInfo info) { _currentIdentifier = info.Identifier; diff --git a/Runtime/Validation/ParameterManagerValidator.cs b/Runtime/Validation/ParameterManagerValidator.cs index 3205e3e..d029ebe 100644 --- a/Runtime/Validation/ParameterManagerValidator.cs +++ b/Runtime/Validation/ParameterManagerValidator.cs @@ -1,11 +1,12 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using PocketGems.Parameters.Interface; using PocketGems.Parameters.Validation.Attributes; -using UnityEngine; +using Debug = UnityEngine.Debug; namespace PocketGems.Parameters.Validation { @@ -93,6 +94,10 @@ public void Validate() where T : class, IBaseInfo private readonly IParameterManager _parameterManager; private readonly List _errors; + // constants + private const int SlowExecutingValidatorInfoMillis = 10; + private const int SlowExecutingValidatorMillis = 100; + // built in attributes private readonly Func[] _builtInAttributesCreators; private readonly IValidationAttribute[] _builtInAttributesInstancePool; @@ -105,6 +110,7 @@ public void Validate() where T : class, IBaseInfo private void ValidateInfoDataValidators(List infos, bool recurseInterfaces, bool callValidateParameters) where T : class, IBaseInfo { + Stopwatch stopwatch = new(); IReadOnlyList interfacesType; if (recurseInterfaces) interfacesType = ParameterManagerValidatorCache.GetAllInterfaceTypes(typeof(T)); @@ -120,7 +126,8 @@ private void ValidateInfoDataValidators(List infos, bool recurseInterfaces var dataValidators = ParameterManagerValidatorCache.GetDataValidatorMap(); if (!dataValidators.ContainsKey(interfaceType)) { - var error = new ValidationError(typeof(T), null, null, $"Cannot find validator for {interfaceType}"); + var error = new ValidationError(typeof(T), null, null, $"Cannot find validator for {interfaceType}", + severity: ValidationError.Severity.Warning); _errors.Add(error); continue; } @@ -135,6 +142,7 @@ private void ValidateInfoDataValidators(List infos, bool recurseInterfaces _dataValidatorVerifiedParameterManagerSet.Add(dataValidator); try { + stopwatch.Restart(); dataValidator.ValidateParameters(_parameterManager); } catch (Exception e) @@ -143,6 +151,15 @@ private void ValidateInfoDataValidators(List infos, bool recurseInterfaces _errors.Add(error); Debug.LogError(e); } + + stopwatch.Stop(); + if (stopwatch.ElapsedMilliseconds >= SlowExecutingValidatorMillis) + { + var error = new ValidationError(typeof(T), null, null, + $"{dataValidator.GetType().Name}.{nameof(dataValidator.ValidateParameters)} took {stopwatch.ElapsedMilliseconds}ms to run validation.", + severity:ValidationError.Severity.Warning); + _errors.Add(error); + } } // get verified set @@ -161,6 +178,7 @@ private void ValidateInfoDataValidators(List infos, bool recurseInterfaces verifiedInfos.Add(info); try { + stopwatch.Restart(); dataValidator.ValidateInfo(_parameterManager, info); } catch (Exception e) @@ -170,6 +188,14 @@ private void ValidateInfoDataValidators(List infos, bool recurseInterfaces _errors.Add(error); Debug.LogError(e); } + + stopwatch.Stop(); + if (stopwatch.ElapsedMilliseconds >= SlowExecutingValidatorInfoMillis) + { + var error = new ValidationError(typeof(T), info.Identifier, null, $"Took {stopwatch.ElapsedMilliseconds}ms to run info validation.", + severity: ValidationError.Severity.Warning); + _errors.Add(error); + } } } } @@ -187,7 +213,8 @@ private void ValidateStructDataValidators(Type structType, List string.IsNullOrEmpty(_message) ? null : _message; public string StructKeyPath => string.IsNullOrEmpty(_structKeyPath) ? null : _structKeyPath; public string StructProperty => string.IsNullOrEmpty(_structProperty) ? null : _structProperty; + public Severity ErrorSeverity => _severity; [NonSerialized] private Type _infoType; [NonSerialized] private bool _loadedType; @@ -40,9 +47,10 @@ public Type InfoType [SerializeField] private string _message; [SerializeField] private string _structKeyPath; [SerializeField] private string _structProperty; + [SerializeField] private Severity _severity; public ValidationError(Type infoType, string infoIdentifier, string infoProperty, string message, - string structKeyPath = null, string structProperty = null) + string structKeyPath = null, string structProperty = null, Severity severity = Severity.Error) { _infoType = infoType; _loadedType = true; @@ -50,9 +58,10 @@ public ValidationError(Type infoType, string infoIdentifier, string infoProperty _typeString = infoType.AssemblyQualifiedName; _infoIdentifier = infoIdentifier; _infoProperty = infoProperty; + _message = message; _structKeyPath = structKeyPath; _structProperty = structProperty; - _message = message; + _severity = severity; } public override string ToString() diff --git a/Tests/Editor/CodeGeneration/Operation/CodeOperationContextTest.cs b/Tests/Editor/CodeGeneration/Operation/CodeOperationContextTest.cs index faa435a..84bb46e 100644 --- a/Tests/Editor/CodeGeneration/Operation/CodeOperationContextTest.cs +++ b/Tests/Editor/CodeGeneration/Operation/CodeOperationContextTest.cs @@ -14,6 +14,7 @@ public void Coverage() Assert.IsNotEmpty(context.SchemaFilePath); Assert.IsNotEmpty(context.GeneratedCodeDir); Assert.IsNotEmpty(context.GeneratedCodeScriptableObjectsDir); + Assert.IsNotEmpty(context.GeneratedCodeStructsDir); Assert.IsNotEmpty(context.GeneratedCodeFlatBufferClassesDir); Assert.IsNotEmpty(context.GeneratedCodeFlatBufferStructsDir); Assert.IsNotEmpty(context.GeneratedCodeFlatBufferBuilderDir); diff --git a/Tests/Editor/Common/Util/ParameterDebugTest.cs b/Tests/Editor/Common/Util/ParameterDebugTest.cs index 8a8716e..58a8e26 100644 --- a/Tests/Editor/Common/Util/ParameterDebugTest.cs +++ b/Tests/Editor/Common/Util/ParameterDebugTest.cs @@ -29,6 +29,13 @@ public void Log() LogAssert.Expect(LogType.Log, _logMessage); } + [Test] + public void LogWarning() + { + ParameterDebug.LogWarning(_logMessage); + LogAssert.Expect(LogType.Warning, _logMessage); + } + [Test] public void LogError() { diff --git a/Tests/Editor/DataGeneration/Operation/DataOperationContextTest.cs b/Tests/Editor/DataGeneration/Operation/DataOperationContextTest.cs index 6893fb4..025cd77 100644 --- a/Tests/Editor/DataGeneration/Operation/DataOperationContextTest.cs +++ b/Tests/Editor/DataGeneration/Operation/DataOperationContextTest.cs @@ -31,6 +31,7 @@ public void Coverage() #endif Assert.AreEqual(0, context.ScriptableObjectMetadatas.Count); Assert.AreEqual(0, context.GeneratedFilePaths.Count); + Assert.AreEqual(0, context.AllValidationErrors.Count); } } } diff --git a/Tests/Runtime/TestCode/TestBaseDataValidator.cs b/Tests/Runtime/TestCode/TestBaseDataValidator.cs index 5e677a1..0492696 100644 --- a/Tests/Runtime/TestCode/TestBaseDataValidator.cs +++ b/Tests/Runtime/TestCode/TestBaseDataValidator.cs @@ -5,17 +5,21 @@ namespace PocketGems.Parameters.Validation public class TestBaseDataValidator : BaseDataValidator where T : class, IBaseInfo { public const string PropertyName = "some property name"; - public const string ErrorMessage1 = "some message"; - public const string ErrorMessage2 = "some other message"; + public const string ErrorMessage1 = "some error"; + public const string ErrorMessage2 = "some other error"; + public const string WarningMessage1 = "some warning"; + public const string WarningMessage2 = "some other warning"; protected override void ValidateInfo(IParameterManager parameterManager, T info) { Error(PropertyName, ErrorMessage1); + Warn(PropertyName, WarningMessage1); } protected override void ValidateParameters(IParameterManager parameterManager) { Error(ErrorMessage2); + Warn(WarningMessage2); } } } diff --git a/Tests/Runtime/Validation/BaseDataValidatorTest.cs b/Tests/Runtime/Validation/BaseDataValidatorTest.cs index c60cb3d..6a375b6 100644 --- a/Tests/Runtime/Validation/BaseDataValidatorTest.cs +++ b/Tests/Runtime/Validation/BaseDataValidatorTest.cs @@ -16,12 +16,21 @@ public void ValidateInfo() validator.ValidateInfo(null, mock); var errors = validator.Errors; - Assert.AreEqual(1, errors.Count); + Assert.AreEqual(2, errors.Count); + var error = errors[0]; Assert.AreEqual(typeof(IMySpecialInfo), error.InfoType); Assert.AreEqual(infoIdentifier, error.InfoIdentifier); + Assert.AreEqual(ValidationError.Severity.Error, error.ErrorSeverity); Assert.AreEqual(TestBaseDataValidator.PropertyName, error.InfoProperty); Assert.AreEqual(TestBaseDataValidator.ErrorMessage1, error.Message); + + var warning = errors[1]; + Assert.AreEqual(typeof(IMySpecialInfo), warning.InfoType); + Assert.AreEqual(infoIdentifier, warning.InfoIdentifier); + Assert.AreEqual(ValidationError.Severity.Warning, warning.ErrorSeverity); + Assert.AreEqual(TestBaseDataValidator.PropertyName, warning.InfoProperty); + Assert.AreEqual(TestBaseDataValidator.WarningMessage1, warning.Message); } [Test] @@ -31,12 +40,21 @@ public void ValidateParameters() validator.ValidateParameters(null); var errors = validator.Errors; - Assert.AreEqual(1, errors.Count); + Assert.AreEqual(2, errors.Count); + var error = errors[0]; Assert.AreEqual(typeof(IMySpecialInfo), error.InfoType); Assert.IsNull(error.InfoIdentifier); Assert.IsNull(error.InfoProperty); + Assert.AreEqual(ValidationError.Severity.Error, error.ErrorSeverity); Assert.AreEqual(TestBaseDataValidator.ErrorMessage2, error.Message); + + var warning = errors[1]; + Assert.AreEqual(typeof(IMySpecialInfo), warning.InfoType); + Assert.IsNull(warning.InfoIdentifier); + Assert.IsNull(warning.InfoProperty); + Assert.AreEqual(ValidationError.Severity.Warning, warning.ErrorSeverity); + Assert.AreEqual(TestBaseDataValidator.WarningMessage2, warning.Message); } } } diff --git a/package.json b/package.json index 26c3ca8..5672466 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.pocketgems.scriptableobject.flatbuffer", - "version": "4.2.1", + "version": "4.3.0", "displayName": "Scriptable Object - FlatBuffer", "description": "Seamless syncing between Scriptable Objects and CSVs. Scriptable Object data built to Google FlatBuffers for optimal runtime loading & access.", "unity": "2021.3",