1 // Licensed to the .NET Foundation under one or more agreements. 2 // The .NET Foundation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System.Text; 6 using System.Data.Common; 7 using System.Globalization; 8 9 namespace System.Data.SqlClient.ManualTesting.Tests 10 { 11 /// <summary> 12 /// represents a base class for SQL type random generation 13 /// </summary> 14 public abstract class SqlRandomTypeInfo 15 { 16 // max size on the row for large blob types to prevent row overflow, when creating the table: 17 // "Warning: The table "TestTable" has been created, but its maximum row size exceeds the allowed maximum of 8060 bytes. INSERT or UPDATE to this table will fail if the resulting row exceeds the size limit." 18 // tests show that the actual size is 36, I added 4 more bytes for extra 19 protected const int LargeVarDataRowUsage = 40; // var types 20 protected const int LargeDataRowUsage = 40; // text/ntext/image 21 protected internal const int XmlRowUsage = 40; // 22 protected const int VariantRowUsage = 40; 23 24 public readonly SqlDbType Type; 25 SqlRandomTypeInfo(SqlDbType t)26 protected SqlRandomTypeInfo(SqlDbType t) 27 { 28 Type = t; 29 } 30 31 /// <summary> 32 /// true if column of this type can be created as sparse column 33 /// </summary> 34 public virtual bool CanBeSparseColumn 35 { 36 get 37 { 38 return true; 39 } 40 } 41 42 43 /// <summary> 44 /// creates a default column instance for given type 45 /// </summary> CreateDefaultColumn()46 public SqlRandomTableColumn CreateDefaultColumn() 47 { 48 return CreateDefaultColumn(SqlRandomColumnOptions.None); 49 } 50 51 /// <summary> 52 /// creates a default column instance for given type 53 /// </summary> CreateDefaultColumn(SqlRandomColumnOptions options)54 public virtual SqlRandomTableColumn CreateDefaultColumn(SqlRandomColumnOptions options) 55 { 56 return new SqlRandomTableColumn(this, options); 57 } 58 59 /// <summary> 60 /// creates a column with random size/precision/scale values, where applicable. 61 /// </summary> 62 /// <remarks>this method is overridden for some types to create columns with random size/precision</remarks> CreateRandomColumn(SqlRandomizer rand, SqlRandomColumnOptions options)63 public virtual SqlRandomTableColumn CreateRandomColumn(SqlRandomizer rand, SqlRandomColumnOptions options) 64 { 65 return CreateDefaultColumn(options); 66 } 67 68 /// <summary> 69 /// helper method to check validity of input column 70 /// </summary> ValidateColumnInfo(SqlRandomTableColumn columnInfo)71 protected void ValidateColumnInfo(SqlRandomTableColumn columnInfo) 72 { 73 if (columnInfo == null) 74 throw new ArgumentNullException("columnInfo"); 75 76 if (Type != columnInfo.Type) 77 throw new ArgumentException("Type mismatch"); 78 } 79 80 /// <summary> 81 /// Returns the size used by a column value within the row. This method is used when generating random table to ensure 82 /// the row size does not overflow 83 /// </summary> GetInRowSize(SqlRandomTableColumn columnInfo, object value)84 public double GetInRowSize(SqlRandomTableColumn columnInfo, object value) 85 { 86 ValidateColumnInfo(columnInfo); 87 88 if (columnInfo.IsSparse) 89 { 90 if (value == DBNull.Value && value == null) 91 { 92 // null values of sparse columns do not use in-row size 93 return 0; 94 } 95 else 96 { 97 // if sparse column has non-null value, it has an additional penalty of 4 bytes added to its storage size 98 return 4 + GetInRowSizeInternal(columnInfo); 99 } 100 } 101 else 102 { 103 // not a sparse column 104 return GetInRowSizeInternal(columnInfo); 105 } 106 } 107 GetInRowSizeInternal(SqlRandomTableColumn columnInfo)108 protected abstract double GetInRowSizeInternal(SqlRandomTableColumn columnInfo); 109 110 /// <summary> 111 /// gets TSQL definition of the column 112 /// </summary> GetTSqlTypeDefinition(SqlRandomTableColumn columnInfo)113 public string GetTSqlTypeDefinition(SqlRandomTableColumn columnInfo) 114 { 115 ValidateColumnInfo(columnInfo); 116 return GetTSqlTypeDefinitionInternal(columnInfo); 117 } 118 GetTSqlTypeDefinitionInternal(SqlRandomTableColumn columnInfo)119 protected abstract string GetTSqlTypeDefinitionInternal(SqlRandomTableColumn columnInfo); 120 121 /// <summary> 122 /// creates random, but valued value for the type, based on the given column definition 123 /// </summary> CreateRandomValue(SqlRandomizer rand, SqlRandomTableColumn columnInfo)124 public object CreateRandomValue(SqlRandomizer rand, SqlRandomTableColumn columnInfo) 125 { 126 ValidateColumnInfo(columnInfo); 127 return CreateRandomValueInternal(rand, columnInfo); 128 } 129 CreateRandomValueInternal(SqlRandomizer rand, SqlRandomTableColumn columnInfo)130 protected abstract object CreateRandomValueInternal(SqlRandomizer rand, SqlRandomTableColumn columnInfo); 131 132 /// <summary> 133 /// helper method to read character data from the reader 134 /// </summary> ReadCharData(DbDataReader reader, int ordinal, Type asType)135 protected object ReadCharData(DbDataReader reader, int ordinal, Type asType) 136 { 137 if (reader.IsDBNull(ordinal)) 138 return DBNull.Value; 139 140 if (asType == typeof(string)) 141 return reader.GetString(ordinal); 142 else if (asType == typeof(char[]) || asType == typeof(DBNull)) 143 return reader.GetString(ordinal).ToCharArray(); 144 else 145 throw new NotSupportedException("Wrong type: " + asType.FullName); 146 } 147 148 /// <summary> 149 /// helper method to read byte-array data from the reader 150 /// </summary> ReadByteArray(DbDataReader reader, int ordinal, Type asType)151 protected object ReadByteArray(DbDataReader reader, int ordinal, Type asType) 152 { 153 if (reader.IsDBNull(ordinal)) 154 return DBNull.Value; 155 156 if (asType == typeof(byte[]) || asType == typeof(DBNull)) 157 return (byte[])reader.GetValue(ordinal); 158 else 159 throw new NotSupportedException("Wrong type: " + asType.FullName); 160 } 161 IsNullOrDbNull(object value)162 protected bool IsNullOrDbNull(object value) 163 { 164 return DBNull.Value.Equals(value) || value == null; 165 } 166 167 /// <summary> 168 /// helper method to check that actual test value has same type as expectedType or it is dbnull. 169 /// </summary> 170 /// <param name="bothDbNull">set to true if both values are DbNull</param> 171 /// <returns>true if expected value is DbNull or has the expected type</returns> CompareDbNullAndType(Type expectedType, object expected, object actual, out bool bothDbNull)172 protected bool CompareDbNullAndType(Type expectedType, object expected, object actual, out bool bothDbNull) 173 { 174 bool isNullExpected = IsNullOrDbNull(expected); 175 bool isNullActual = IsNullOrDbNull(actual); 176 177 bothDbNull = isNullActual && isNullExpected; 178 179 if (bothDbNull) 180 return true; 181 182 if (isNullActual || isNullExpected) 183 return false; // only one is null, but not both 184 185 if (expectedType == null) 186 return true; 187 188 // both not null 189 if (expectedType != expected.GetType()) 190 throw new ArgumentException("Wrong type!"); 191 192 return (expectedType == actual.GetType()); 193 } 194 195 /// <summary> 196 /// helper method to compare two byte arrays 197 /// </summary> 198 /// <remarks>I considered use of Generics here, but switched to explicit typed version due to performance overhead. 199 /// When using generic version, there is no way to quickly compare two values (expected[i] == actual[i]), and using 200 /// Equals method performs boxing, increasing the time spent on this method.</remarks> CompareByteArray(byte[] expected, byte[] actual, bool allowIncomplete = false, byte paddingValue = 0)201 private bool CompareByteArray(byte[] expected, byte[] actual, bool allowIncomplete = false, byte paddingValue = 0) 202 { 203 if (expected.Length > actual.Length) 204 { 205 return false; 206 } 207 else if (!allowIncomplete && expected.Length < actual.Length) 208 { 209 return false; 210 } 211 212 // check expected array values 213 int end = expected.Length; 214 for (int i = 0; i < end; i++) 215 { 216 if (expected[i] != actual[i]) 217 return false; 218 } 219 220 // check for padding in actual values 221 end = actual.Length; 222 for (int i = expected.Length; i < end; i++) 223 { 224 // ensure rest of array are zeros 225 if (paddingValue != actual[i]) 226 { 227 return false; 228 } 229 } 230 231 return true; 232 } 233 234 /// <summary> 235 /// helper method to compare two char arrays 236 /// </summary> 237 /// <remarks>I considered use of Generics here, but switched to explicit typed version due to performance overhead. 238 /// When using generic version, there is no way to quickly compare two values (expected[i] == actual[i]), and using 239 /// Equals method performs boxing, increasing the time spent on this method.</remarks> CompareCharArray(char[] expected, char[] actual, bool allowIncomplete = false, char paddingValue = B)240 private bool CompareCharArray(char[] expected, char[] actual, bool allowIncomplete = false, char paddingValue = ' ') 241 { 242 if (expected.Length > actual.Length) 243 { 244 return false; 245 } 246 else if (!allowIncomplete && expected.Length < actual.Length) 247 { 248 return false; 249 } 250 251 // check expected array values 252 int end = expected.Length; 253 for (int i = 0; i < end; i++) 254 { 255 if (expected[i] != actual[i]) 256 return false; 257 } 258 259 // check for padding in actual values 260 end = actual.Length; 261 for (int i = expected.Length; i < end; i++) 262 { 263 // ensure rest of array are zeros 264 if (paddingValue != actual[i]) 265 { 266 return false; 267 } 268 } 269 270 return true; 271 } 272 273 /// <summary> 274 /// helper method to compare two non-array values. 275 /// </summary> 276 protected bool CompareValues<T>(object expected, object actual) where T : struct 277 { 278 bool bothDbNull; 279 if (!CompareDbNullAndType(typeof(T), expected, actual, out bothDbNull) || bothDbNull) 280 return bothDbNull; 281 282 return expected.Equals(actual); 283 } 284 285 286 /// <summary> 287 /// validates that the actual value is DbNull or byte array and compares it to expected 288 /// </summary> CompareByteArray(object expected, object actual, bool allowIncomplete, byte paddingValue = 0)289 protected bool CompareByteArray(object expected, object actual, bool allowIncomplete, byte paddingValue = 0) 290 { 291 bool bothDbNull; 292 if (!CompareDbNullAndType(typeof(byte[]), expected, actual, out bothDbNull) || bothDbNull) 293 return bothDbNull; 294 return CompareByteArray((byte[])expected, (byte[])actual, allowIncomplete, paddingValue); 295 } 296 297 /// <summary> 298 /// validates that the actual value is DbNull or char array and compares it to expected 299 /// </summary> CompareCharArray(object expected, object actual, bool allowIncomplete, char paddingValue = B)300 protected bool CompareCharArray(object expected, object actual, bool allowIncomplete, char paddingValue = ' ') 301 { 302 bool bothDbNull; 303 if (!CompareDbNullAndType(typeof(char[]), expected, actual, out bothDbNull) || bothDbNull) 304 return bothDbNull; 305 return CompareCharArray((char[])expected, (char[])actual, allowIncomplete, paddingValue); 306 } 307 308 /// <summary> 309 /// helper method to reads datetime from the reader 310 /// </summary> ReadDateTime(DbDataReader reader, int ordinal, Type asType)311 protected object ReadDateTime(DbDataReader reader, int ordinal, Type asType) 312 { 313 ValidateReadType(typeof(DateTime), asType); 314 if (reader.IsDBNull(ordinal)) 315 return DBNull.Value; 316 return reader.GetDateTime(ordinal); 317 } 318 ValidateReadType(Type expectedType, Type readAsType)319 protected void ValidateReadType(Type expectedType, Type readAsType) 320 { 321 if (readAsType != expectedType && readAsType != typeof(DBNull)) 322 throw new ArgumentException("Wrong type: " + readAsType.FullName); 323 } 324 325 /// <summary> 326 /// this method is called to read the value from the data reader 327 /// </summary> Read(DbDataReader reader, int ordinal, SqlRandomTableColumn columnInfo, Type asType)328 public object Read(DbDataReader reader, int ordinal, SqlRandomTableColumn columnInfo, Type asType) 329 { 330 if (reader == null || asType == null) 331 throw new ArgumentNullException("reader == null || asType == null"); 332 ValidateColumnInfo(columnInfo); 333 return ReadInternal(reader, ordinal, columnInfo, asType); 334 } 335 ReadInternal(DbDataReader reader, int ordinal, SqlRandomTableColumn columnInfo, Type asType)336 protected abstract object ReadInternal(DbDataReader reader, int ordinal, SqlRandomTableColumn columnInfo, Type asType); 337 338 /// <summary> 339 /// used to check if this column can be compared; returns true by default (timestamp and column set columns return false) 340 /// </summary> CanCompareValues(SqlRandomTableColumn columnInfo)341 public virtual bool CanCompareValues(SqlRandomTableColumn columnInfo) 342 { 343 return true; 344 } 345 346 /// <summary> 347 /// This method is called to compare the actual value read to expected. Expected value must be either dbnull or from the given type. 348 /// Actual value can be any. 349 /// </summary> CompareValues(SqlRandomTableColumn columnInfo, object expected, object actual)350 public bool CompareValues(SqlRandomTableColumn columnInfo, object expected, object actual) 351 { 352 ValidateColumnInfo(columnInfo); 353 return CompareValuesInternal(columnInfo, expected, actual); 354 } 355 CompareValuesInternal(SqlRandomTableColumn columnInfo, object expected, object actual)356 protected abstract bool CompareValuesInternal(SqlRandomTableColumn columnInfo, object expected, object actual); 357 BuildErrorMessage(SqlRandomTableColumn columnInfo, object expected, object actual)358 public string BuildErrorMessage(SqlRandomTableColumn columnInfo, object expected, object actual) 359 { 360 ValidateColumnInfo(columnInfo); 361 362 string expectedAsString = IsNullOrDbNull(expected) ? "null" : "\"" + ValueAsString(columnInfo, expected) + "\""; 363 string actualAsString = IsNullOrDbNull(actual) ? "null" : "\"" + ValueAsString(columnInfo, actual) + "\""; 364 365 return string.Format(CultureInfo.InvariantCulture, 366 " Column type: {0}\n" + 367 " Expected value: {1}\n" + 368 " Actual value: {2}", 369 GetTSqlTypeDefinition(columnInfo), 370 expectedAsString, 371 actualAsString); 372 } 373 374 /// <summary> 375 /// using ToString by default, supports arrays and many primitives; 376 /// override as needed 377 /// </summary> 378 /// <param name="value">value is not null or dbnull(validated before)</param> ValueAsString(SqlRandomTableColumn columnInfo, object value)379 protected virtual string ValueAsString(SqlRandomTableColumn columnInfo, object value) 380 { 381 Array a = value as Array; 382 if (a == null) 383 { 384 return string.Format(CultureInfo.InvariantCulture, "{0}: {1}", value.GetType().Name, PrimitiveValueAsString(value)); 385 } 386 else 387 { 388 StringBuilder sb = new StringBuilder(); 389 sb.AppendFormat("{0}[{1}]", value.GetType().Name, a.Length); 390 if (a.Length > 0) 391 { 392 sb.Append(":"); 393 for (int i = 0; i < a.Length; i++) 394 { 395 sb.AppendFormat(CultureInfo.InvariantCulture, " {0}", PrimitiveValueAsString(a.GetValue(i))); 396 } 397 } 398 399 return sb.ToString(); 400 } 401 } 402 PrimitiveValueAsString(object value)403 public string PrimitiveValueAsString(object value) 404 { 405 if (value is char) 406 { 407 int c = (int)(char)value; // double-cast is needed from object 408 return c.ToString("X4", CultureInfo.InvariantCulture); 409 } 410 else if (value is byte) 411 { 412 byte b = (byte)value; 413 return b.ToString("X2", CultureInfo.InvariantCulture); 414 } 415 else 416 { 417 return string.Format(CultureInfo.InvariantCulture, "{0}", value.ToString()); 418 } 419 } 420 } 421 } 422