Выложил на BitBucket проект AcadKeyParser. Информацию о назначении библиотеки, примеры её использования и откомпилированные версии под .NET 3.5, 4.0, 4.5 и 4.6 разместил там же. В составе решения присутствуют модульные тесты (NUnit) и консольная утилита, позволяющая интерактивно проверять "валидность" ключей AutoCAD. Результаты модульных тестов автоматом оформляются в виде HTML отчёта при помощи утилиты ReportUnit (более детальную информацию см. в файле run_me.bat).
Архив рубрики: NUnit
NUnit: вывод сообщений трассировщика
По умолчанию, GUI NUnit не отображает сообщения трассировщика. Однако это поведение можно изменить в настройках программы.
Для сборок Release я, обычно, предпочитаю отключать компиляцию кода трассировки.
Но для сборок, собранных в режиме DEBUG, обозначенную директиву компиляции всегда полезно активировать. Итак, для того, чтобы GUI NUnit отображал сообщения трассировщика, встречающиеся в нашем исходном коде, например: что-то вроде этого:
CultureInfo culture = null;
if (!valid_lcids.TryGetValue(lcid, out culture)) {
Trace.TraceError(String.Format("Incorrect LCID value: {0}.\n", lcid));
return false;
}
необходимо в настройках NUnit включить соответствующую галочку. Группа интересующих нас настроек (как видим, она позволяет управлять отображением контента и из др. потоков):
Обозначенный ниже текст выведен именно трассировщиком:
NUnit: Тестирование DLL, использующей внешние данные
В составе одной из моих DLL присутствует функционал, позволяющий проверять на предмет корректности выражения вида ACAD-E001:409 (а так же создавать их) и, если выражение корректно, то полностью расшифровывать эту информацию. Для работы обозначенной DLL необходим специальный XML файл, содержащий информацию о существующих версиях AutoCAD и их вертикальных продуктах. Этот файл размещается в том же каталоге, в котором находится DLL. Для тестирования обозначенного функционала я написал пару тестов и запустил их в GUI NUnit...
Код моей DLL находит нужный XML следующим образом:
Результаты компиляции размещены в каталоге "C:\Users\developer\Documents\Visual Studio 2013\Projects\cad-solution\cad.UnitTests\bin\Debug". Запустив тесты на исполнение я увидел, что все они с треском провалились:
В правом верхнем текстбоксе читаю сгенерированное моим кодом сообщение об ошибке:
Bushman.CAD.UnitTests.Tests.LocalizedProduct_TryParse_Check_BoolValue(null,False):
System.TypeInitializationException : Инициализатор типа "Bushman.CAD.LocalizedProduct" выдал исключение.
----> System.IO.FileNotFoundException : C:\Users\developer\AppData\Local\Temp\nunit20\ShadowCopyCache\5128_635702471073123992\Tests_27930185\assembly\dl3\7e84bc6f\b85de2a2_d6a9d001\cad.data.xml
Я подсветил красным цветом ключевую информацию. Это исключение происходит потому, что NUnit копирует тестируемую DLL в произвольно сгенерированный каталог и уже оттуда загружает её и запускает присутствующие в ней тесты. Но скопировав DLL, NUnit не сделал того же самого и для моего XML файла, в виду чего и происходит обозначенное исключение. Обозначенное поведение в NUnit называется Shadow Copy (Теневое Копирование).
Shadow Copy можно включать или отключать в настройках NUnit: меню Tools -> Settings... -> Test Loader -> Advanced -> Shadow Copy -> Enable Shadow Copy:
Обратите внимание на информацию, которую я выделил красным цветом.
Если обозначенную галочку снять, то после перезапуска NUnit теневое копирование не происходит и DLL загружается из того каталога, где она изначально находилась. Соответственно успешно находится и XML файл:
Код моей DLL находит нужный XML следующим образом:
String asmLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly(
).Location);
// xml file with AutoCAD products data.
String xmlFile = Path.Combine(asmLocation, "cad.data.xml");
if (!File.Exists(xmlFile)) {
throw new FileNotFoundException(xmlFile);
}
Результаты компиляции размещены в каталоге "C:\Users\developer\Documents\Visual Studio 2013\Projects\cad-solution\cad.UnitTests\bin\Debug". Запустив тесты на исполнение я увидел, что все они с треском провалились:
В правом верхнем текстбоксе читаю сгенерированное моим кодом сообщение об ошибке:
Bushman.CAD.UnitTests.Tests.LocalizedProduct_TryParse_Check_BoolValue(null,False):
System.TypeInitializationException : Инициализатор типа "Bushman.CAD.LocalizedProduct" выдал исключение.
----> System.IO.FileNotFoundException : C:\Users\developer\AppData\Local\Temp\nunit20\ShadowCopyCache\5128_635702471073123992\Tests_27930185\assembly\dl3\7e84bc6f\b85de2a2_d6a9d001\cad.data.xml
Я подсветил красным цветом ключевую информацию. Это исключение происходит потому, что NUnit копирует тестируемую DLL в произвольно сгенерированный каталог и уже оттуда загружает её и запускает присутствующие в ней тесты. Но скопировав DLL, NUnit не сделал того же самого и для моего XML файла, в виду чего и происходит обозначенное исключение. Обозначенное поведение в NUnit называется Shadow Copy (Теневое Копирование).
Shadow Copy можно включать или отключать в настройках NUnit: меню Tools -> Settings... -> Test Loader -> Advanced -> Shadow Copy -> Enable Shadow Copy:
Обратите внимание на информацию, которую я выделил красным цветом.
Если обозначенную галочку снять, то после перезапуска NUnit теневое копирование не происходит и DLL загружается из того каталога, где она изначально находилась. Соответственно успешно находится и XML файл:
ReportUnit на смену NUnitOrange
Отличная новость: Anshoo Arora создал новое приложение: ReportUnit, которое является продвинутой заменой NUnitOrange. Теперь на основе XML файлов, генерируемых платформами NUnit, Gallio и MSTest можно генерировать единообразно и весьма удобно оформленные HTML отчёты о результатах тестирования (в т.ч. и сводный).
Принцип использования ReportUnit тот же самый, что и у NUnitOrange. Скачать ZIP архив с приложением можно отсюда. Web-страничка проекта находится здесь. Исходный код проекта доступен на GitHub.
Ну и, собственно, небольшой скрин, демонстрирующий оформление результатов модульного тестирования:
Принцип использования ReportUnit тот же самый, что и у NUnitOrange. Скачать ZIP архив с приложением можно отсюда. Web-страничка проекта находится здесь. Исходный код проекта доступен на GitHub.
Ну и, собственно, небольшой скрин, демонстрирующий оформление результатов модульного тестирования:
TestCase в NUnit и Gallio
В NUnit, TestCase - это атрибут, однако в Gallio это не так. Т.о. в каждой из обозначенных платформ своя реализация поведения обозначенного элемента. В этой заметке показан пример исходного кода, который успешно компилируется и работает как в случае использования Gallio, так и в случае использования NUnit.
Хорошую книгу о том, как грамотно создавать автономные и интеграционные тесты, в т.ч. и под такие закрытые системы как AutoCAD, я указывал здесь, в п.21. Кроме того, Gallio имеет неплохую offline документацию, доступную в меню Пуск -> Все программы -> Gallio -> Offline Documentation, а документация по NUnit присутствует online.
Gallio может выдавать отчёты о результатах теста как в формате XML, так и в формате HTML. В то же время NUnit может генерировать только XML. Для получения HTML на основе XML отчётов NUnit я рекомендую пользоваться утилитой NUnitOrange.
Полагаю, что лучше всего различие продемонстрирует исходный код:
/* © Andrey Bushman, 2015
* Tests.cs
* Recommended format of naming of tests:
* UnitOfWorkName_Scenario_ExpectedBehavior
*/
#if !ENTRY_POINT
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Text;
using System.Threading;
using System.Windows.Controls;
using System.Windows.Converters;
using System.Windows.Forms;
using System.Windows;
#if NUNIT
using Fw = NUnit.Framework;
#elif GALLIO
using Gallio.Framework;
using Gallio.Model;
using Fw = MbUnit.Framework;
#endif
#if TEIGHA_CLASSIC
using Db = Teigha.DatabaseServices;
using Rt = Teigha.Runtime;
using Gm = Teigha.Geometry;
#endif
#if NANOCAD
using cad = HostMgd.ApplicationServices.Application;
using Ap = HostMgd.ApplicationServices;
using Ed = HostMgd.EditorInput;
#elif BRICSCAD
using cad = Bricscad.ApplicationServices.Application;
using Ap = Bricscad.ApplicationServices;
using Ed = Bricscad.EditorInput;
#elif AUTOCAD
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Ap = Autodesk.AutoCAD.ApplicationServices;
using Db = Autodesk.AutoCAD.DatabaseServices;
using Ed = Autodesk.AutoCAD.EditorInput;
using Rt = Autodesk.AutoCAD.Runtime;
using Gm = Autodesk.AutoCAD.Geometry;
using Br = Autodesk.AutoCAD.BoundaryRepresentation;
using Hs = Autodesk.AutoCAD.DatabaseServices.HostApplicationServices;
using Us = Autodesk.AutoCAD.DatabaseServices.SymbolUtilityServices;
#endif
#if AUTOCAD_NEWER_THAN_2012
using corecad = Autodesk.AutoCAD.ApplicationServices.Core.Application;
#endif
#if AUTOCAD && (PLATFORM_x64 || PLATFORM_x86)
using In = Autodesk.AutoCAD.Interop;
using Ic = Autodesk.AutoCAD.Interop.Common;
#endif
using Ex = Bushman.CAD.Extensions.ExtensionSample.UnitTests;
namespace Bushman.CAD.Extensions.ExtensionSample.UnitTests {
[Fw.TestFixture,
#if NUNIT
Fw.Apartment(ApartmentState.STA)
#endif
]
public class Tests {
const String blockName = "TEMP_BLOCK";
// ***********************************************************************
[Fw.Ignore("Sample of ignored test.")]
[Fw.Test]
[Fw.Category("Autodesk API")]
[Fw.Description("Some description")]
public void HasAttributeDefinitions_WhenAttribsExist_IsTrue() {
// Create new temp database
using (Db.Database db = new Db.Database(true, true)) {
using (new WorkingDatabaseSwitcher(db)) {
using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
Db.ObjectId id = CreateBlockDefinition(db);
Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForRead) as
Db.BlockTableRecord;
Fw.Assert.IsTrue(btr.HasAttributeDefinitions);
tr.Commit();
}
}
} // close and discard changes
}
[Fw.Test]
[Fw.Category("Autodesk API")]
[Fw.Description("This test shows AutoCAD .NET API bug. It exists in " +
"AutoCAD 2009-2016.")]
public void HasAttributeDefinitions_WhenAttribsInNotExist_IsFalse() {
Db.ObjectId id = Db.ObjectId.Null;
// Create new temp database
using (Db.Database db = new Db.Database(true, true)) {
using (new WorkingDatabaseSwitcher(db)) {
using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
// create new block definition with an attribute definition
id = CreateBlockDefinition(db);
Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForWrite) as
Db.BlockTableRecord;
// remove all attribute definitions
String name = Rt.RXClass.GetClass(typeof(Db.AttributeDefinition))
.Name;
foreach (Db.ObjectId itemId in btr) {
if (itemId.ObjectClass.Name == name) {
Db.DBObject obj = tr.GetObject(itemId, Db.OpenMode.ForWrite);
obj.Erase(true);
}
}
tr.Commit();
}
}
// Check attribute definition count
using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForWrite) as
Db.BlockTableRecord;
Fw.Assert.IsFalse(btr.HasAttributeDefinitions);
tr.Commit();
}
} // close and discard changes
}
// ***********************************************************************
[Fw.Test]
[Fw.Category("Bushman API")]
[Fw.Description("I am some description 1 :)")]
public void HasAttDefs_WhenAttribsExist_IsTrue() {
// Create new temp database
using (Db.Database db = new Db.Database(true, true)) {
using (new WorkingDatabaseSwitcher(db)) {
using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
Db.ObjectId id = CreateBlockDefinition(db);
Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForRead) as
Db.BlockTableRecord;
Fw.Assert.IsTrue(btr.HasAttDefs());
tr.Commit();
}
}
} // close and discard changes
}
[Fw.Test]
[Fw.Category("Bushman API")]
[Fw.Description("I am some description 2 :)")]
public void HasAttDefs_WhenAttribsIsNotExist_IsFalse() {
Db.ObjectId id = Db.ObjectId.Null;
// Create new temp database
using (Db.Database db = new Db.Database(true, true)) {
using (new WorkingDatabaseSwitcher(db)) {
using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
id = CreateBlockDefinition(db);
Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForWrite) as
Db.BlockTableRecord;
// remove all attribute definitions
String name = Rt.RXClass.GetClass(typeof(Db.AttributeDefinition))
.Name;
foreach (Db.ObjectId itemId in btr) {
if (itemId.ObjectClass.Name == name) {
Db.DBObject obj = tr.GetObject(itemId, Db.OpenMode.ForWrite);
obj.Erase(true);
}
}
tr.Commit();
}
// Check attribute definition count
using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForWrite) as
Db.BlockTableRecord;
Fw.Assert.IsFalse(btr.HasAttDefs());
tr.Commit();
}
}
} // close and discard changes
}
// **********************************************************************
// Creating of the temp block with an instance of AttributeDefinition
internal static Db.ObjectId CreateBlockDefinition(Db.Database db) {
if (null == db || db.IsDisposed) {
throw new ArgumentException("null == db || db.IsDisposed");
}
Db.ObjectId id = Db.ObjectId.Null;
// Create a temp block definition with an AttributeDefinition instance
using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
Db.BlockTable bt = tr.GetObject(db.BlockTableId, Db.OpenMode.ForWrite
) as Db.BlockTable;
Db.BlockTableRecord btr = new Db.BlockTableRecord();
btr.Name = blockName;
// its content: the circle and attribite definition
Db.Circle circle = new Db.Circle();
circle.SetDatabaseDefaults();
circle.Radius = 20.0;
circle.Center = Gm.Point3d.Origin;
circle.ColorIndex = 50;
btr.AppendEntity(circle);
Db.AttributeDefinition atDef = new Db.AttributeDefinition(
circle.Center, "Hello!", "ATTRIB", "New value",
Us.GetTextStyleStandardId(db));
atDef.SetDatabaseDefaults();
btr.AppendEntity(atDef);
id = bt.Add(btr);
tr.AddNewlyCreatedDBObject(btr, true);
tr.Commit();
}
return id;
}
// **********************************************************************
// This test template reads the Database from the DWG file for own work.
[Fw.Test]
[Fw.Category("TestCase using samples")]
public void CircleIsExists() {
const String dwgFileName = @"..\NUnit\data-for-testing\data_01.dwg";
Boolean result = false;
// Read Database from the DWG file
using (Db.Database db = new Db.Database(true, true)) {
db.ReadDwgFile(dwgFileName, Db.FileOpenMode.OpenForReadAndWriteNoShare,
false, "");
using (new WorkingDatabaseSwitcher(db)) {
using (Db.Transaction tr = db.TransactionManager.StartTransaction())
{
Db.ObjectId msId = Us.GetBlockModelSpaceId(db);
Db.BlockTableRecord ms = tr.GetObject(msId, Db.OpenMode.ForRead)
as Db.BlockTableRecord;
Rt.RXClass rxc = Rt.RXClass.GetClass(typeof (Db.Circle));
Db.ObjectId id = ms.Cast<Db.ObjectId>().FirstOrDefault();
result = Db.ObjectId.Null != id;
tr.Commit();
}
Fw.Assert.IsTrue(result);
}
} // close and discard changes
}
// **********************************************************************
#if GALLIO
[Fw.StaticTestFactory]
public static IEnumerable<Fw.Test> TestSuite_RenameMe() {
yield return new Fw.TestSuite("Gallio tests some suite") {
Description = "An example test suite.",
Metadata =
{
{ MetadataKeys.Category, "TestCase using samples" },
{ MetadataKeys.Description, "I am some description 3 :)" }
},
Timeout = TimeSpan.FromMinutes(2),
Children =
{
new Fw.TestCase("CircleMustToBeExisting", () => {
__CheckingOfCircleExisting(@"..\NUnit\data-for-testing\data_01.dwg", true);
}),
new Fw.TestCase("CircleMustNotToBeExisting", () => {
__CheckingOfCircleExisting(@"..\NUnit\data-for-testing\data_02.dwg", false);
})
}
};
}
#elif NUNIT
[Fw.Test]
[Fw.Category("TestCase using samples")]
[Fw.TestCase(@"..\NUnit\data-for-testing\data_01.dwg", true)]
[Fw.TestCase(@"..\NUnit\data-for-testing\data_02.dwg", false)]
#endif
public void CheckingOfCircleExisting(String dwgFileName,
Boolean expectedResult) {
__CheckingOfCircleExisting(dwgFileName, expectedResult);
}
static void __CheckingOfCircleExisting(String dwgFileName,
Boolean expectedResult) {
// Read Database from the DWG file
using (Db.Database db = new Db.Database(true, true)) {
db.ReadDwgFile(dwgFileName, Db.FileOpenMode.OpenForReadAndWriteNoShare,
false, "");
Boolean result = false;
using (new WorkingDatabaseSwitcher(db)) {
using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
Db.ObjectId msId = Us.GetBlockModelSpaceId(db);
Db.BlockTableRecord ms = tr.GetObject(msId, Db.OpenMode.ForRead)
as Db.BlockTableRecord;
Rt.RXClass rxc = Rt.RXClass.GetClass(typeof(Db.Circle));
result = ms.Cast<Db.ObjectId>().Any(n=>n.ObjectClass.IsDerivedFrom(rxc));
tr.Commit();
}
Fw.Assert.AreEqual(expectedResult, result);
}
} // close and discard changes
}
// **********************************************************************
}
}
#endif
Результаты тестов, в пакетном режиме автоматически произведённых в AutoCAD 2009-2015 и представленных в формате HTML можно скачать и посмотреть отсюда. Пакетное тестирование происходило с использованием acad.exe для AutoCAD 2009-2012 и accoreconsole.exe для AutoCAD 2013-2015. Тесты для AutoCAD 2009 и 2010 скомпилированы с использованием платформы Gallio. Тесты для AutoCAD 2011-2015 собраны с использованием NUnit. Внешнее представление отчётов в HTML-формате у этих платформ несколько отличается, но оба варианта достаточно удобны для использования.
В коде тестов я активно использую один из своих вспомогательных классов: WorkingDatabaseSwitcher. Вот его исходный код:
/* © Andrey Bushman, 2015
* WorkingDatabaseSwitcher.cs
*/
#if !ENTRY_POINT
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Text;
using System.Threading;
using System.Windows.Controls;
using System.Windows.Converters;
using System.Windows.Forms;
using System.Windows;
#if NUNIT
using Fw = NUnit.Framework;
#elif GALLIO
using Gallio.Framework;
using Fw = MbUnit.Framework;
#endif
#if TEIGHA_CLASSIC
using Db = Teigha.DatabaseServices;
using Rt = Teigha.Runtime;
using Gm = Teigha.Geometry;
#endif
#if NANOCAD
using cad = HostMgd.ApplicationServices.Application;
using Ap = HostMgd.ApplicationServices;
using Ed = HostMgd.EditorInput;
#elif BRICSCAD
using cad = Bricscad.ApplicationServices.Application;
using Ap = Bricscad.ApplicationServices;
using Ed = Bricscad.EditorInput;
#elif AUTOCAD
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Ap = Autodesk.AutoCAD.ApplicationServices;
using Db = Autodesk.AutoCAD.DatabaseServices;
using Ed = Autodesk.AutoCAD.EditorInput;
using Rt = Autodesk.AutoCAD.Runtime;
using Gm = Autodesk.AutoCAD.Geometry;
using Br = Autodesk.AutoCAD.BoundaryRepresentation;
using Hs = Autodesk.AutoCAD.DatabaseServices.HostApplicationServices;
using Us = Autodesk.AutoCAD.DatabaseServices.SymbolUtilityServices;
#endif
#if AUTOCAD_NEWER_THAN_2012
using corecad = Autodesk.AutoCAD.ApplicationServices.Core.Application;
#endif
#if AUTOCAD && (PLATFORM_x64 || PLATFORM_x86)
using In = Autodesk.AutoCAD.Interop;
using Ic = Autodesk.AutoCAD.Interop.Common;
#endif
using Ex = Bushman.CAD.Extensions.ExtensionSample.UnitTests;
namespace Bushman.CAD.Extensions.ExtensionSample.UnitTests {
/// <summary>
/// This class switches the WorkingDatabase. It was created for using in the
/// tests.
/// </summary>
internal sealed class WorkingDatabaseSwitcher : IDisposable {
Db.Database oldDb = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="db">Target database.</param>
public WorkingDatabaseSwitcher(Db.Database db) {
oldDb = Hs.WorkingDatabase;
Hs.WorkingDatabase = db;
}
public void Dispose() {
Hs.WorkingDatabase = oldDb;
}
}
}
#endif
В обозначенных выше тестах я тестирую в том числе и некоторый метод HasAttDefs(), вот его исходный код:
/* © Andrey Bushman, 2015
* ExtensionMethods.cs
*/
#if !ENTRY_POINT
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Text;
using System.Windows.Controls;
using System.Windows.Converters;
using System.Windows.Forms;
using System.Windows;
#if TEIGHA_CLASSIC
using Db = Teigha.DatabaseServices;
using Rt = Teigha.Runtime;
using Gm = Teigha.Geometry;
#endif
#if NANOCAD
using cad = HostMgd.ApplicationServices.Application;
using Ap = HostMgd.ApplicationServices;
using Ed = HostMgd.EditorInput;
#elif BRICSCAD
using cad = Bricscad.ApplicationServices.Application;
using Ap = Bricscad.ApplicationServices;
using Ed = Bricscad.EditorInput;
#elif AUTOCAD
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Ap = Autodesk.AutoCAD.ApplicationServices;
using Db = Autodesk.AutoCAD.DatabaseServices;
using Ed = Autodesk.AutoCAD.EditorInput;
using Rt = Autodesk.AutoCAD.Runtime;
using Gm = Autodesk.AutoCAD.Geometry;
using Br = Autodesk.AutoCAD.BoundaryRepresentation;
using Hs = Autodesk.AutoCAD.DatabaseServices.HostApplicationServices;
using Us = Autodesk.AutoCAD.DatabaseServices.SymbolUtilityServices;
#endif
#if AUTOCAD_NEWER_THAN_2009
using In = Autodesk.AutoCAD.Internal;
#endif
#if AUTOCAD_NEWER_THAN_2012
using corecad = Autodesk.AutoCAD.ApplicationServices.Core.Application;
#endif
#if AUTOCAD && (PLATFORM_x64 || PLATFORM_x86)
using In = Autodesk.AutoCAD.Interop;
using Ic = Autodesk.AutoCAD.Interop.Common;
#endif
using Ex = Bushman.CAD.Extensions.ExtensionSample;
namespace Bushman.CAD.Extensions.ExtensionSample {
public static class ExtensionMethods {
/// <summary>
/// This method is a replace for the
/// <c>BlockTableRecord.HasAttributeDefinitions</c> method. Implementation
/// by Autodesk works wrong: it returns <c>True</c> after the
/// <c>AttributeDefinition</c> instance was deleted from the
/// <c>BlockTableRecord</c>. Info source:
/// http://bushman-andrey.blogspot.ru/2014/03/blocktablerecordhasattributedefinitions.html
/// </summary>
/// <param name="btr">Target instance of the <c>BlockTableRecord</c> class.
/// </param>
/// <returns>returns true or false.</returns>
public static Boolean HasAttDefs(this Db.BlockTableRecord btr) {
String name = Rt.RXClass.GetClass(typeof(Db.AttributeDefinition)).Name;
return btr.Cast<Db.ObjectId>().Any(n => !n.IsNull && n.IsValid
&& !n.IsErased && !n.IsEffectivelyErased && String.Equals(
n.ObjectClass.Name, name, StringComparison.InvariantCulture));
}
}
}
#endif
Шаблон проекта VS для написания модульных тестов для .Net-расширений AutoCAD
Ранее я уже приводил пример создания общего шаблона для .NET плагина под любую версию AutoCAD не старше чем 2009-я. Аналогичный шаблон можно создать и для модульных тестов под эти плагины.
В качестве платформы тестирования для управляемых расширений [плагинов] AutoCAD можно использовать Gallio или NUnit.
Gallio благополучно работает с любой версией AutoCAD новее чем 2008 (я не проверял для версий старее чем AutoCAD 2009-й). Однако разработка Gallio на сегодняшний день приостановлена. Тем не менее его можно успешно продолжать использовать. Исходники Gallio опубликованы на GitHub и доступны для изучения\изменения. Однако Gallio работает только с acad.exe - использовать accoreconsole.exe не удастся.
NUnit успешно работает начиная с AutoCAD 2011 и во всех более новых версиях. Версии AutoCAD 2011 - 2014 требуют предварительной установки переменной NEXTFIBERWORLD в значение 0 с последующим перезапуском AutoCAD. По завершению работы тестов, не забудьте переменной NEXTFIBERWORLD снова назначить в качестве значения 1.
Начиная с AutoCAD 2015 компания Autodesk уходит от использования фиберов, поэтому версии AutoCAD, новее чем 2014-я не требуют предварительного изменения обозначенной переменной для успешной работы тестов. Поскольку версии AutoCAD 2010 и все более старые не имеют переменной NEXTFIBERWORLD, то использовать в них тесты NUnit не представляется возможным.
Начиная с AutoCAD 2015 компания Autodesk уходит от использования фиберов, поэтому версии AutoCAD, новее чем 2014-я не требуют предварительного изменения обозначенной переменной для успешной работы тестов. Поскольку версии AutoCAD 2010 и все более старые не имеют переменной NEXTFIBERWORLD, то использовать в них тесты NUnit не представляется возможным.
В отличие от Gallio, NUnit может работать как с acad.exe, так и с accoreconsole.exe.
Т.о. в AutoCAD 2009 и 2010 следует использовать тестовую платформу Gallio, в то время как начиная с AutoCAD 2011 можно использовать либо Gallio, либо NUnit по вашему желанию (я предпочитаю NUnit).
Один и тот же исходный код модульных тестов может компилироваться под разные платформы тестирования. По аналогии с шаблоном плагинов AutoCAD, который я демонстрировал в видео ранее, можно создать шаблон для модульных тестов управляемых расширений AutoCAD. Такой шаблон позволяет пакетно компилировать один и тот же исходный код модульных тестов под разные версии AutoCAD с использованием как платформы Gallio, так и платформы NUnit. В приведённом ниже примере для AutoCAD 2009 и 2010 я генерирую тесты с использованием Gallio, а для AutoCAD 2011-2015 - с использованием NUnit. Оба проекта построены на основе соответствующих шаблонов: первый - на основе шаблона для .NET расширений AutoCAD, а второй - на основе шаблона для создания модульных тестов для .NET расширений AutoCAD.
Дополнительно генерируется набор BAT-файлов, каждый из которых предназначен для запуска тестов в конкретной версии AutoCAD. Результаты тестирования оформляются в виде отчёта в формате HTML. На мой взгляд всё получается достаточно удобно.
Когда-то я писал о баге, присутствующем в AutoCAD .NET API и давал свой вариант обходного решения. Обозначенное ниже видео построено на основе этого кода: тесты выявляют баг в API от Autodesk, а так же проверяют работоспособность моего варианта решения.
UPD:
Добавил генерацию BAT-файла, который последовательно выполняет тесты во всех нужных версиях AutoCAD (в данном случае AutoCAD 2009-2015). Вот видео на эту тему:
Далее привожу код тестов данного видео:
Добавил генерацию BAT-файла, который последовательно выполняет тесты во всех нужных версиях AutoCAD (в данном случае AutoCAD 2009-2015). Вот видео на эту тему:
Далее привожу код тестов данного видео:
/* © Andrey Bushman, 2015
* Tests.cs
*/
#if !ENTRY_POINT
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Text;
using System.Threading;
using System.Windows.Controls;
using System.Windows.Converters;
using System.Windows.Forms;
using System.Windows;
#if NUNIT
using Fw = NUnit.Framework;
#elif GALLIO
using Gallio.Framework;
using Fw = MbUnit.Framework;
#endif
#if TEIGHA_CLASSIC
using Db = Teigha.DatabaseServices;
using Rt = Teigha.Runtime;
using Gm = Teigha.Geometry;
#endif
#if NANOCAD
using cad = HostMgd.ApplicationServices.Application;
using Ap = HostMgd.ApplicationServices;
using Ed = HostMgd.EditorInput;
#elif BRICSCAD
using cad = Bricscad.ApplicationServices.Application;
using Ap = Bricscad.ApplicationServices;
using Ed = Bricscad.EditorInput;
#elif AUTOCAD
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Ap = Autodesk.AutoCAD.ApplicationServices;
using Db = Autodesk.AutoCAD.DatabaseServices;
using Ed = Autodesk.AutoCAD.EditorInput;
using Rt = Autodesk.AutoCAD.Runtime;
using Gm = Autodesk.AutoCAD.Geometry;
using Br = Autodesk.AutoCAD.BoundaryRepresentation;
using Hs = Autodesk.AutoCAD.DatabaseServices.HostApplicationServices;
using Us = Autodesk.AutoCAD.DatabaseServices.SymbolUtilityServices;
#endif
#if AUTOCAD_NEWER_THAN_2012
using corecad = Autodesk.AutoCAD.ApplicationServices.Core.Application;
#endif
#if AUTOCAD && (PLATFORM_x64 || PLATFORM_x86)
using In = Autodesk.AutoCAD.Interop;
using Ic = Autodesk.AutoCAD.Interop.Common;
#endif
using Ex = Bushman.CAD.Extensions.CAD.ExtensionMethods.UnitTests;
namespace Bushman.CAD.Extensions.CAD.ExtensionMethods.UnitTests {
[Fw.TestFixture,
#if NUNIT
Fw.Apartment(ApartmentState.STA)
#endif
]
public class Tests {
const String blockName = "TEMP_BLOCK";
// ***********************************************************************
[Fw.Test]
[Fw.Category("Autodesk API")]
public void HasAttributeDefinitions_WhenAttribsExist_IsTrue() {
// Create new temp database
using (Db.Database db = new Db.Database(true, true)) {
using (new WorkingDatabaseSwitcher(db)) {
using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
Db.ObjectId id = CreateBlockDefinition(db);
Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForRead) as
Db.BlockTableRecord;
Fw.Assert.IsTrue(btr.HasAttributeDefinitions);
tr.Commit();
}
}
} // close and discard changes
}
// [Fw.Ignore("We can't fix this bug, because it is by Autodesk.")]
[Fw.Test]
[Fw.Category("Autodesk API")]
public void HasAttributeDefinitions_WhenAttribsInNotExist_IsFalse() {
Db.ObjectId id = Db.ObjectId.Null;
// Create new temp database
using (Db.Database db = new Db.Database(true, true)) {
using (new WorkingDatabaseSwitcher(db)) {
using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
// create new block definition with an attribute definition
id = CreateBlockDefinition(db);
Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForWrite) as
Db.BlockTableRecord;
// remove all attribute definitions
String name = Rt.RXClass.GetClass(typeof(Db.AttributeDefinition))
.Name;
foreach (Db.ObjectId itemId in btr) {
if (itemId.ObjectClass.Name == name) {
Db.DBObject obj = tr.GetObject(itemId, Db.OpenMode.ForWrite);
obj.Erase(true);
}
}
tr.Commit();
}
}
// Check attribute definition count
using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForWrite) as
Db.BlockTableRecord;
Fw.Assert.IsFalse(btr.HasAttributeDefinitions);
tr.Commit();
}
} // close and discard changes
}
// ***********************************************************************
[Fw.Test]
[Fw.Category("Bushman API")]
public void HasAttDefs_WhenAttribsExist_IsTrue() {
// Create new temp database
using (Db.Database db = new Db.Database(true, true)) {
using (new WorkingDatabaseSwitcher(db)) {
using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
Db.ObjectId id = CreateBlockDefinition(db);
Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForRead) as
Db.BlockTableRecord;
Fw.Assert.IsTrue(btr.HasAttDefs());
tr.Commit();
}
}
} // close and discard changes
}
[Fw.Test]
[Fw.Category("Bushman API")]
public void HasAttDefs_WhenAttribsIsNotExist_IsFalse() {
Db.ObjectId id = Db.ObjectId.Null;
// Create new temp database
using (Db.Database db = new Db.Database(true, true)) {
using (new WorkingDatabaseSwitcher(db)) {
using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
id = CreateBlockDefinition(db);
Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForWrite) as
Db.BlockTableRecord;
// remove all attribute definitions
String name = Rt.RXClass.GetClass(typeof(Db.AttributeDefinition))
.Name;
foreach (Db.ObjectId itemId in btr) {
if (itemId.ObjectClass.Name == name) {
Db.DBObject obj = tr.GetObject(itemId, Db.OpenMode.ForWrite);
obj.Erase(true);
}
}
tr.Commit();
}
// Check attribute definition count
using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
Db.BlockTableRecord btr = tr.GetObject(id, Db.OpenMode.ForWrite) as
Db.BlockTableRecord;
Fw.Assert.IsFalse(btr.HasAttDefs());
tr.Commit();
}
}
} // close and discard changes
}
// **********************************************************************
// Creating of the temp block with an instance of AttributeDefinition
internal static Db.ObjectId CreateBlockDefinition(Db.Database db) {
if (null == db || db.IsDisposed) {
throw new ArgumentException("null == db || db.IsDisposed");
}
Db.ObjectId id = Db.ObjectId.Null;
// Create a temp block definition with an AttributeDefinition instance
using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
Db.BlockTable bt = tr.GetObject(db.BlockTableId, Db.OpenMode.ForWrite
) as Db.BlockTable;
Db.BlockTableRecord btr = new Db.BlockTableRecord();
btr.Name = blockName;
// its content: the circle and attribite definition
Db.Circle circle = new Db.Circle();
circle.SetDatabaseDefaults();
circle.Radius = 20.0;
circle.Center = Gm.Point3d.Origin;
circle.ColorIndex = 50;
btr.AppendEntity(circle);
Db.AttributeDefinition atDef = new Db.AttributeDefinition(
circle.Center, "Hello!", "ATTRIB", "New value",
Us.GetTextStyleStandardId(db));
atDef.SetDatabaseDefaults();
btr.AppendEntity(atDef);
id = bt.Add(btr);
tr.AddNewlyCreatedDBObject(btr, true);
tr.Commit();
}
return id;
}
}
}
#endif