четверг, 26 марта 2015 г.

Шаблон проекта 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 не представляется возможным.

В отличие от 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). Вот видео на эту тему:



Далее привожу код тестов данного видео:

/* © 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(truetrue)) {
        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(truetrue)) {
        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(truetrue)) {
        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(truetrue)) {
        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

Комментариев нет: