2009/10/26

VB6でDoxygen

VB6をレガシーコードからモダンコードに移行させるには、
ユニットテストとコードドキュメント生成が必須となる。
今回は、コードドキュメント生成をDoxygenで行う。
といっても、Doxygen単体ではVB6コード扱えない。
そこでC風コードや、Java風コードに変換しなければならない。
いくつか試した中でも、 vbfilter.py が一番精度が高かったので以後EXEとして利用する。
  1. Doxygenをセットアップ
    本家からセットアップバイナリを落として、セットアップ






    とりあえず、必要なのはDoxywizardのみ。後はお好きにどうぞ。









  2. Doxygenファイルを作成する
    作成には、メニューのDoxywizardを実行する


    プロジェクト概要を設定する。
    設定しておくのは、プロジェクト名くらいかと。


    生成対象ソースコード形式を設定する。
    vbfilterの仕様では、C++に設定する。


    出力フォーマットを設定する。
    出力オプションはお好みでどうぞ。


    グラフ出力等にGraphvizを利用する場合は、設定する。
    ちなみに、Doxygen自体にもグラフ出力機能は同梱されている。


    プロジェクト詳細情報を設定する。
    DOXYFILE_ENCODING値をUTF-8にしないと出力出来ない模様


    bas、cls、frmを追加する。
    ※INPUT_FILTER値には、それぞれvbfilter.exeを設定する

  3. VB6なコードを記述する
    記述仕様:
    • クラス概要は、!@brief
    • メソッド、オブジェクト、変数概要は、*@brief
    • メソッド引数は、*@param
    • 覚書は、@note
    • グラフは、@dot

  4. ドキュメントを生成する
    実行すると、Warning等と進捗がログ表示される
    出力先は、設定ディレクトリ下「html」ディレクトリになる


    プロジェクト名が出力されている


    クラス一覧。クラス概要が出力されている


    クラスメソッド一覧


    概要コメントが出力されている


    グラフ出力、各種詳細コメントが出力されている

2009/10/25

Py2exeでvbfilter.pyをexe化する

pythonicなコードは、一般的な会社等での配布&運用が難しいと思われる。
面倒でもpy2exeを通して、exeとして配布することでMSな人々でも抵抗無く利用出来る。
VB6をDoxygenするのに必要なvbfilterをexe化したメモ。
  1. Pythonをセットアップ
    Pythonicな人はしてあるとは思うけど...何それ?的な人は、PyJUGからどうぞ(^-^)

  2. Py2exeをセットアップ
    http://python.matrix.jp/modules/py2exe.htmlからどうぞ

  3. vbfitlerを用意する
    こちらからどうぞ。Doxygen記事で参考にしました。

  4. setup.pyを作成する

    from distutils.core import setup
    import py2exe
    
    py2exe_options = {
      "compressed": 1,
      "optimize": 2,
      "bundle_files": 2}
    
    setup(
      options = {"py2exe": py2exe_options},
      console = [
        {"script" : "vbfilter.py"}],
      zipfile = None)  
    

  5. buildを実行する
    setup.py py2exe
以上でvbfilter.exeが作成される。
これを利用すれば、ちょっとした作業程度ならダラダラと
dotnetやVBA(VB6含み)の使い捨てコードを書かなくて済む。

[注意点 ]
TracLightning等がインストールされていると、
Python系環境変数はそちらが優先されることが多い。
従って、別途Pythonを追加インストールしてexe作成作業するには、
参照切り替えを行うorライブラリ系参照パスを追加すればよい

Tracで簡易WBS

timingandestimationpluginを導入することで、簡易な工数管理が行える。
しかし、ガントチャートでは工数等が見えない為、WBS的な利用には今一歩といったところ。
そこで、無理矢理ガントチャートへ要りそうな拡張を行う。
とりあえず、欲しい機能は...
  1. 各チケット毎に実績と見積を表示する
  2. チケットの実績と見積のそれぞれの合計を表示する
  3. 実績が見積を超過した場合は、分かりやすく表示する
  4. 進捗率をプログレスバー的な表示以外に、数字として表示する
に絞る。
完成画面:

という訳で、実装する。
当然、編集対象プラグインはganttcalendarになる。
プラグイン構成は、
  • ganntcalendar
    • ticketcalendar.py
    • ticketgannt.py  <- 今回編集する
    • templates
      • gantt.html <- 今回編集する
      • calendar.html
以下、実装後ファイル
[ticketgantt.py]
# -*- coding: utf-8 -*-
import re, calendar, time, sys
from datetime import datetime, date, timedelta
from genshi.builder import tag

from trac.core import *
from trac.web import IRequestHandler
from trac.web.chrome import INavigationContributor, ITemplateProvider
from trac.util.datefmt import to_datetime, format_date, parse_date

class TicketGanttChartPlugin(Component):
    implements(INavigationContributor, IRequestHandler, ITemplateProvider)

    # INavigationContributor methods
    def get_active_navigation_item(self, req):
        return 'ticketgantt'
    
    def get_navigation_items(self, req):
        if req.perm.has_permission('TICKET_VIEW'):
            yield ('mainnav', 'ticketgantt',tag.a(u'ガントチャート', href=req.href.ticketgantt()))

    # IRequestHandler methods
    def match_request(self, req):
        return re.match(r'/ticketgantt(?:_trac)?(?:/.*)?$', req.path_info)

    def adjust( self, x_start, x_end, term):
        if x_start > term or x_end < 0:
            x_start= done_end= None
        else:
            if x_start < 0:
                x_start= 0
            if x_end > term:
                x_end= term
        return x_start, x_end

    def process_request(self, req):
        req.perm.assert_permission('TICKET_VIEW')
        self.log.debug("process_request " + str(globals().get('__file__')))
        ymonth = req.args.get('month')
        yyear = req.args.get('year')
        baseday = req.args.get('baseday')
        selected_milestone = req.args.get('selected_milestone')
        selected_component = req.args.get('selected_component')
        show_my_ticket = req.args.get('show_my_ticket')
        show_closed_ticket = req.args.get('show_closed_ticket')
        sorted_field = req.args.get('sorted_field')
        if sorted_field == None:
            sorted_field = 'component'

        if baseday != None:
            baseday = parse_date( baseday).date()
        else:
            baseday = date.today()

        cday = date.today()
        if not (not ymonth or not yyear):
            cday = date(int(yyear),int(ymonth),1)

        # cal next month
        nmonth = cday.replace(day=1).__add__(timedelta(days=32)).replace(day=1)

        # cal previous month
        pmonth = cday.replace(day=1).__add__(timedelta(days=-1)).replace(day=1)

        first_date= cday.replace(day=1)
        days_term= (first_date.__add__(timedelta(100)).replace(day=1)-first_date).days

        # process ticket
        db = self.env.get_db_cnx()
        cursor = db.cursor();
        sql = ""
        condition=""
        if show_my_ticket == 'on':
            if condition != "":
                condition += " AND "
            condition += "owner ='" + req.authname + "'"
        if show_closed_ticket != 'on':
            if condition != "":
                condition += " AND "
            condition += "status <> 'closed'"
        if selected_milestone != None and selected_milestone !="":
            if condition != "":
                condition += " AND "
            condition += "milestone ='" + selected_milestone +"'"
        if selected_component != None and selected_component !="":
            if condition != "":
                condition += " AND "
            condition += "component ='" + selected_component +"'"

        if condition != "":
            condition = "WHERE " + condition + " "

        sql = ("SELECT id, type, summary, owner, t.description, status, a.value, c.value, cmp.value, milestone, component, etime.value, ttime.value "
                "FROM ticket t "
                "JOIN ticket_custom a ON a.ticket = t.id AND a.name = 'due_assign' "
                "JOIN ticket_custom c ON c.ticket = t.id AND c.name = 'due_close' "
                "JOIN ticket_custom cmp ON cmp.ticket = t.id AND cmp.name = 'complete' "
                "JOIN ticket_custom etime ON etime.ticket = t.id AND etime.name = 'estimatedhours' "
                "JOIN ticket_custom ttime ON ttime.ticket = t.id AND ttime.name = 'totalhours' "
                "%sORDER by %s , a.value ") % (condition, sorted_field)

        self.log.debug(sql)
        cursor.execute(sql)

        tickets=[]
        estimatedhour_sum = 0.0
        totalhour_sum = 0.0
        for id, type, summary, owner, description, status, due_assign, due_close, complete, milestone, component, estimatedhours, totalhours in cursor:
            due_assign_date = None
            due_close_date = None
            try:
                due_assign_date = parse_date(due_assign).date()
            except ( TracError, ValueError, TypeError):
                continue
            try:
                due_close_date = parse_date(due_close).date()
            except ( TracError, ValueError, TypeError):
                continue
            if complete != None and len(complete)>1 and complete[len(complete)-1]=='%':
                complete = complete[0:len(complete)-1]
            try:
                if int(complete) >100:
                    complete = "100"
            except:
                complete = "0"
            complete = int(complete)
            if due_assign_date > due_close_date:
                continue
            if milestone == None or milestone == "":
                milestone = "*"
            if component == None or component == "":
                component = "*"
            if estimatedhours == None or estimatedhours < 0:
                estimatedhours = 0
            else:
                estimatedhours = float(estimatedhours)
            if totalhours == None or totalhours < 0:
                totalhours = 0
            else:
                totalhours = float(totalhours)
            estimatedhour_sum += estimatedhours
            totalhour_sum += totalhours

            ticket = {'id':id, 'type':type, 'summary':summary, 'owner':owner, 'description': description, 'status':status,
                    'due_assign':due_assign_date, 'due_close':due_close_date, 'complete': complete, 
                    'milestone': milestone,'component': component,
                    'estimatedhours':estimatedhours,
                    'totalhours':totalhours}
            #calc chart
            base = (baseday -first_date).days + 1
            done_start= done_end= None
            late_start= late_end= None
            todo_start= todo_end= None
            all_start=(due_assign_date-first_date).days
            all_end=(due_close_date-first_date).days + 1
            done_start= all_start
            done_end= done_start + (all_end - all_start)*int(complete)/100.0
            if all_end <= base < days_term:
                late_start= done_end
                late_end= all_end
            elif done_end <= base < all_end:
                late_start= done_end
                late_end= todo_start= base
                todo_end= all_end
            else:
                todo_start= done_end
                todo_end= all_end
            #
            done_start, done_end= self.adjust(done_start,done_end,days_term)
            late_start, late_end= self.adjust(late_start,late_end,days_term)
            todo_start, todo_end= self.adjust(todo_start,todo_end,days_term)
            all_start, all_end= self.adjust(all_start,all_end,days_term)

            if done_start != None:
                ticket.update({'done_start':done_start,'done_end':done_end})
            if late_start != None:
                ticket.update({'late_start':late_start,'late_end':late_end})
            if todo_start != None:
                ticket.update({'todo_start':todo_start,'todo_end':todo_end})
            if all_start != None:
                ticket.update({'all_start':all_start})

            self.log.debug(ticket)
            tickets.append(ticket)

        # milestones
        milestones = {'':None}
        sql = ("SELECT name, due, completed, description FROM milestone")
        self.log.debug(sql)
        cursor.execute(sql)
        for name, due, completed, description in cursor:
            due_date = to_datetime(due, req.tz).date()
            item = { 'due':due_date, 'completed':completed != 0,'description':description}
            if due==0:
                del item['due']
            milestones.update({name:item})
        # componet
        components = [{}]
        sql = ("SELECT name FROM component")
        self.log.debug(sql)
        cursor.execute(sql)
        for name, in cursor:
            components.append({'name':name})

        holidays = {}
        sql = "SELECT date,description from holiday"
        try:
            cursor.execute(sql)
            for hol_date,hol_desc in cursor:
                holidays[format_date(parse_date(hol_date, tzinfo=req.tz))]= hol_desc
        except:
            pass

        data = {'baseday': baseday, 'current':cday, 'prev':pmonth, 'next':nmonth}
        data.update({'show_my_ticket': show_my_ticket, 'show_closed_ticket': show_closed_ticket, 'sorted_field': sorted_field})
        data.update({'selected_milestone':selected_milestone,'selected_component': selected_component})
        data.update({'tickets':tickets,'milestones':milestones,'components':components})
        data.update({'holidays':holidays,'first_date':first_date,'days_term':days_term})
        data.update({'parse_date':parse_date,'format_date':format_date,'calendar':calendar})
        data.update({'estimatedhour_sum':estimatedhour_sum,'totalhour_sum':totalhour_sum})
        return 'gantt.html', data, None

    def get_templates_dirs(self):
        from pkg_resources import resource_filename
        return [resource_filename(__name__, 'templates')]

    def get_htdocs_dirs(self):
        from pkg_resources import resource_filename
        return [('tc', resource_filename(__name__, 'htdocs'))]

[gantt.html]
<が&lt;な所に注意!
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/"
      xmlns:xi="http://www.w3.org/2001/XInclude"
      py:with="px_ti=25;px_hd=46;px_dw=12;px_ch=10;maxtic=len(tickets);">
  <xi:include href="layout.html" />
  <xi:include href="macros.html" />
  <head>
    <script type="text/javascript" src="${chrome.htdocs_location}js/folding.js"></script>
    <script type="text/javascript">
      jQuery(document).ready(function($) {
        $("fieldset legend.foldable").enableFolding(false);
        /* Hide the filters for saved queries. */
        $("#options").toggleClass("collapsed");
      });
    </script>

    <style type="text/css">
      form fieldset.collapsed { 
        border-width: 0px;
        margin-bottom: 0px;
        padding: 0px .5em;
      }
      fieldset legend.foldable :link,
      fieldset legend.foldable :visited { 
        background: url(${chrome.htdocs_location}expanded.png) 0 50% no-repeat;
        border: none;
        color: #666;
        font-size: 110%;
        padding-left: 16px;
      }
      fieldset legend.foldable :link:hover, fieldset legend.foldable :visited:hover {
        background-color: transparent;
      }
      
      fieldset.collapsed legend.foldable :link, fieldset.collapsed legend.foldable :visited { 
        background-image: url(${chrome.htdocs_location}collapsed.png);  
      }
      fieldset.collapsed table, fieldset.collapsed div { display: none }
      table.list     {width:100%;border-collapse: collapse;margin-bottom: 6px;}
      table.list td  {padding:2px;}
      .border_line   {background-color: gray;}
      .hdr           {position: absolute;background-color: #eee;}
      .hdr_title     {display:block;position: absolute; width:100%;text-align:center;font-size:10px;}
      .bdy           {position: absolute;background-color: #fff;text-align: left;top:${px_hd}px;height:${maxtic*px_ti}px;}
      .bdy_elem      {position: absolute;font-size: 9px;left:1px;height:${px_ti-2}px;}

      .tip           {position: static;}
      .tip span.popup{position: absolute;visibility: hidden;background-color: #ffe;color: black;border: 1px solid #555;left: 20px;top: 30px;width: 400px; padding: 3px;}
      .tip:hover span.popup
                     {visibility: visible; z-index: 100;}

      .tic_done      {position: absolute; overflow: hidden; background:lightgreen;}
      .tic_late      {position: absolute; overflow: hidden; background:pink;}
      .tic_todo      {position: absolute; overflow: hidden; background:lightgrey;}
      .tic_done_bl   {position: absolute; overflow: hidden; background:green;}
      .tic_late_bl   {position: absolute; overflow: hidden; background:red;}
      .tic_todo_bl   {position: absolute; overflow: hidden; background:gray;}

      .baseline      {position: absolute; overflow: hidden; border-left: 1px dashed red;}
      .milestone  {position: absolute; overflow: hidden; background-color: red;}
    </style>
  </head>
  <body py:with="weekdays = ['月', '火', '水' ,'木', '金', '土', '日']">
    <form>
      <fieldset id="options">
        <legend class="foldable">設定</legend>
        <table class="list">
          <tr>
            <td>
              基準日<input type="text" id="field-baseday" name="baseday" value="${format_date(parse_date(baseday.isoformat()))}" length="10"/>
            </td>
          </tr>
          <tr>
            <td>
              ソートするフィールド
              <select name="sorted_field">
                <option value="component" selected="${sorted_field=='component' or None}">component</option>
                <option value="milestone" selected="${sorted_field=='milestone' or None}">milestone</option>
              </select><br/>
            </td>
          </tr>
          <tr>
            <td>
              絞込みをします
              マイルストーン = 
              <select name="selected_milestone">
              <py:for each="i in milestones.keys()">
                <option selected="${selected_milestone==i or None}" value="$i">$i</option>
              </py:for>
              </select>
              AND 
              コンポーネント =
              <select name="selected_component">
              <py:for each="i in components">
                  <option selected="${selected_component==i.name or None}" value="${i.name}">${i.name}</option>
              </py:for>
              </select>
            </td>
          </tr>
          <tr>
            <td>
              <input type="checkbox" name="show_my_ticket" checked="$show_my_ticket" />自分のチケットのみ表示
              <input type="checkbox" name="show_closed_ticket" checked="$show_closed_ticket" />closeしたチケットを表示<br/>
  
            </td>
            <td align="right" valign="bottom">
              <input type="submit" value="更新" />
            </td>
          </tr>
        </table>
      </fieldset>
      <table class="list">
        <tr>
          <td>
            <input type="button" value="<<${prev.month}月" onclick="form.year.value = ${prev.year}; form.month.value = ${prev.month}; form.submit();"/>
          </td>
          <td align="center">
            <select name="year">
              <option py:for="y in range(current.year-3,current.year+4)"
                     value="$y"
                     selected="${y==current.year or None}">$y</option>
            </select>
            年
            <select name="month">
              <option py:for="m in [1,2,3,4,5,6,7,8,9,10,11,12]"
                     value="$m" selected="${m==current.month or None}">$m</option>
            </select>
            月
            <input type="submit" value="更新" />
          </td>
          <td align="right">
            <input type="button" value="${next.month}月>>" onclick="form.year.value = ${next.year}; form.month.value = ${next.month}; form.submit();"/>
          </td>
        </tr>
      </table>
    </form>
    <!-- gantt -->
    <div style="position:relative;left:1px;top:1px;width:100%;height:${maxtic*px_ti+px_hd+1+40}px;">
      <!-- right side -->
      <div style="overflow:auto;margin-left:403px;margin-right:4px;position:relative;left:0px;top:1px;height:${maxtic*px_ti+px_hd+1+30}px;">
        <div class="border_line" style="left:0px;top:1px;width:${px_dw*days_term+1}px;height:${maxtic*px_ti+px_hd+1}px;">
          <!-- head and sun,sta,holiday -->
          <div class="bdy" style="position:relative;left:1px;top:${px_hd}px;width:${px_dw*days_term-1}px;height:${maxtic*px_ti}px;"/>
<py:for each="cnt in reversed(range(days_term))" py:with="cur=first_date+timedelta(cnt);wk=cur.weekday()">
          <div py:if="cur.day == 1" py:with="days_thismonth=calendar.monthrange(cur.year,cur.month)[1];" class="hdr hdr_title" style="left:${px_dw*cnt+1}px;top:${(px_hd-4)/3*0+1}px;width: ${days_thismonth*px_dw-1}px;height:${(px_hd-4)/3}px;">${cur.year}/${cur.month}</div>
          <div py:if="wk==6 and(cur-first_date).days+7<days_term" class="hdr hdr_title" style="left:${px_dw*cnt+1}px;top:${(px_hd-4)/3*1+2}px;width: ${px_dw*7-1}px;height:${(px_hd-4)/3}px;">${cur.month}/${cur.day}</div>
          <div py:if="wk==6 and(cur-first_date).days+7>days_term" class="hdr hdr_title" style="left:${px_dw*cnt+1}px;top:${(px_hd-4)/3*1+2}px;width: ${px_dw*(days_term-(cur-first_date).days)-1}px;height:${(px_hd-4)/3}px;"/>
          <div class="hdr hdr_title" style="left:${px_dw*cnt+1}px;top:${(px_hd-4)/3*2+3}px;width: ${px_dw-1}px;height:${(px_hd-4)/3}px;">${weekdays[wk]}</div>
  <py:with vars="holiday_desc = holidays.get(format_date(parse_date(cur.isoformat())));">
    <py:if test="cur.weekday()>4or holiday_desc">
          <div class="border_line" style="position:absolute;top:${px_hd}px; left: ${px_dw*cnt}px; width: ${px_dw+1}px; height: ${maxtic*px_ti+1}px;">
            <div class="hdr" py:attrs="{'title':holiday_desc}" style="top:0px; left:1px; width: ${px_dw-1}px; height: ${maxtic*px_ti}px;"/>
          </div>
    </py:if>
  </py:with>
</py:for>
          <div py:if="first_date.weekday()!=6" class="hdr" style="left:1px;top:${(px_hd-4)/3*1+2}px;width: ${px_dw*(6-first_date.weekday())-1}px;height:${(px_hd-4)/3}px;"/>
          <!-- chart -->
<py:def function="print_chart(kind)">
  <py:with vars="s=tickets[cnt].get('all_start');e=tickets[cnt].get(kind +'_end');">
    <py:if test="e!=None and e-s!= 0">
          <div class="${'tic_'+kind+'_bl'}" style="left:${int(s*px_dw+1)}px;top:${px_ti*cnt+px_hd+((px_ti-px_ch)/2)}px;width: ${int((e-s)*px_dw)}px;height:${px_ch}px;"/>
          <div class="${'tic_'+kind}" style="left:${int(s*px_dw+2)}px;top:${px_ti*cnt+px_hd+((px_ti-px_ch)/2+1)}px;width: ${int((e-s)*px_dw)-2}px;height:${px_ch-2}px;"/>
    </py:if>
  </py:with>
</py:def>
<py:for each="cnt in reversed(range(maxtic))">
          ${print_chart('todo')}
          ${print_chart('late')}
          ${print_chart('done')}
  <py:if test="selected_milestone != '' and selected_milestone != None">
    <py:if test="tickets[cnt].get('milestone')!= None and tickets[cnt].get('milestone') in milestones" py:with="d= milestones[tickets[cnt]['milestone']].get('due')">
      <py:if test="d!=None and 0 <= (d-first_date).days+1 < days_term" py:with="d=(d-first_date).days+1">
          <div class="milestone" style="left: ${d*px_dw}px; top: ${cnt*px_ti+px_hd}px;  width: 2px; height: ${px_ti}px;"></div>
      </py:if>
    </py:if>
  </py:if>
</py:for>
<py:with vars="base = (baseday-first_date).days+1">
          <!-- baseline -->
          <div py:if="base+1 < days_term" class="baseline" style="left:${base*px_dw}px;top:${px_hd}px; height:${maxtic*px_ti}px; width: 0px;"/>
</py:with>
        </div>
      </div>
      <!-- left side -->
      <div style="position:absolute;background-color:gray;left:1px;top:1px;width:402px;height:${maxtic*px_ti+px_hd+1}px;">
        <div class="hdr" style="left:1px;top:1px;width: 89px;height:${px_hd-2}px;"><span class="hdr_title" style="top:${(px_hd-2-16)/2}px;">${_(sorted_field)}</span></div>
        <div class="hdr" style="left:91px;top:1px;width:104px;height:${px_hd-2}px;"><span class="hdr_title" style="top:${(px_hd-2-16)/2}px;">チケット</span></div>
        <div class="hdr" style="left:196px;top:1px;width:107px;height:${px_hd-2}px;"><span class="hdr_title" style="top:${(px_hd-2-16)/2}px;">担当者</span></div>
        <div class="hdr" style="left:304px;top:1px;width:35px;height:${px_hd-2}px;"><span class="hdr_title" style="top:${(px_hd-2-16)/2}px;">実績</span></div>
        <div class="hdr" style="left:340px;top:1px;width:35px;height:${px_hd-2}px;"><span class="hdr_title" style="top:${(px_hd-2-16)/2}px;">見積</span></div>
        <div class="hdr" style="left:376px;top:1px;width:25px;height:${px_hd-2}px;"><span class="hdr_title" style="top:${(px_hd-2-16)/2}px;">達成</span></div>
<py:def function="print_field(px_x,px_w,ticket_col,dupchk=False)">
        <div class="bdy" style="left:${px_x}px;width:${px_w}px;">
  <py:for each="cnt in range(maxtic)" py:with="t=tickets[cnt]">
    <py:choose>
      <py:when test="ticket_col=='ticket'" py:with="cnt=maxtic-cnt-1;t=tickets[cnt];">
          <div class="bdy_elem" style="top: ${cnt*px_ti}px;width: ${px_w-2}px;">
            <a class="tip" href="${req.href.ticket()}/${t['id']}">#${t['id']}:${t['summary'][0:14]}
              <span class="popup">
                <pre><span class="type">${t['type']}</span>#${t['id']}: ${t['summary']}</pre>
                <strong>担当者</strong>: ${format_author(t['owner'])}<br/>
                <strong>開始日</strong>: ${format_date(parse_date(t['due_assign'].isoformat()))}<br/>
                <strong>終了日</strong>: ${format_date(parse_date(t['due_close'].isoformat()))}<br/>
                <strong>達成率</strong>: ${t['complete']}%<br/>
                <strong>作業実績</strong>: ${t['totalhours']}<br/>
                <strong>予定工数</strong>: ${t['estimatedhours']}<br/>
                <strong>詳細</strong>: <pre>${t['description']}</pre>
              </span>
            </a>
          </div>
      </py:when>
      <py:otherwise>
        <py:choose>
          <py:when test="dupchk">
          <div py:if="not cnt or t[ticket_col]!=tickets[cnt-1][ticket_col]" class="bdy_elem" style="top:${cnt*px_ti}px;width: ${px_w-2}px;">${t[ticket_col]}</div>
          </py:when>
          <py:otherwise>
            <div class="bdy_elem" style="top: ${cnt*px_ti}px;width: ${px_w-2}px;">${ticket_col in ('owner','reporter') and format_author(t[ticket_col]) or t[ticket_col]}</div>
          </py:otherwise>
        </py:choose>
      </py:otherwise>
    </py:choose>
  </py:for>
        </div>
</py:def>
<py:def function="print_time_fields(ttime_x,etime_x,col_width)">
  <py:def function="print_time_value(val,cnt,color='')">
    <div class="bdy_elem" style="top: ${cnt*px_ti}px;width: ${col_width-2}px;${color}">$val</div>
</py:def>
  <py:def function="print_totaltime_value(t,cnt,color='')">
    ${print_time_value('totalhours' in ('owner','reporter') and format_author(t['totalhours']) or t['totalhours'],cnt,color)}
  </py:def>
  <div class="bdy" style="left:${ttime_x}px;width:${col_width}px;">
    <py:for each="cnt in range(maxtic)" py:with="t=tickets[cnt]">
 <py:choose>
   <py:when test="t['totalhours'] &<= t['estimatedhours']">
     ${print_totaltime_value(t,cnt)}
   </py:when>
   <py:otherwise>
     ${print_totaltime_value(t,cnt,'color:red;')}
   </py:otherwise>
 </py:choose>
    </py:for>
    <div class="bdy_elem" style="top: ${maxtic*px_ti}px;width: ${col_width-2}px;">$totalhour_sum</div>
  </div>
  <div class="bdy" style="left:${etime_x}px;width:${col_width}px;">
    <py:for each="cnt in range(maxtic)" py:with="t=tickets[cnt]">
    ${print_time_value('estimatedhours' in ('owner','reporter') and format_author(t['estimatedhours']) or t['estimatedhours'],cnt)}
    </py:for>
    <div class="bdy_elem" style="top: ${maxtic*px_ti}px;width: ${col_width-2}px;">$estimatedhour_sum</div>
  </div>
</py:def>
        ${print_field( 376,25,'complete')}
        ${print_time_fields(304,340,35)}
        ${print_field( 196,107,'owner')}
        ${print_field(  91,104,'ticket')}
        ${print_field(   1, 89,sorted_field,dupchk=True)}
      </div>
    </div>
    <!-- gantt -->
  </body>
</html>

2009/10/19

CodeRush

Visual Studio 2005 時代は、Refactor!がVB.Netにとっての唯一リファクタリングツールだった。
2008時代は、CodeRushがその荷を背負うことになった。
その試食メモ。
  1. まずはセットアップ
    本家からバイナリをダウンロードして、セットアップする
    ちなみに、セットアップ開始前に必ずVisualStudiを終了しなければならない










  2. 早速VisualStudioを起動してみる
    メソッドの横に縦線がメソッド区切りの可視化として表示される


    コンテキストメニューに、「Refactor!」が追加されている...


    試しに実行してみると、「Refactor!」そのものが動く模様
  3. リファクタリングしてみる
    簡単そうな名前変更を試す


    「GetMessage」から「GetHelloMessage」に変更する


    自プロジェクトのみではなく、ソリューション全体がリファクタリング対象
    ということで、TestProjectの方も名前変更されている(^-^)

TestDriven.net

VB6ではIDEと連携したUnitTestフレームワークは一般的ではなく、
最悪、UnitTestって何デスカ?状態だった...
しかし、dotnetに移行することで容易にUnitTestを出来るようになった。
そんな訳で、TestDriven.netのセットアップと運用メモ。
  1. まずはセットアップ
    本家からセットアップバイナリをダウンロードし、セットアップ











  2. 早速使ってみる
    まずは、コンテキストメニューにアイテムが追加されていること確認


  3. テスト対象クラスとメソッドを作成


  4. テスト用プロジェクトを作成し、テストクラスを作成する






  5. テスト対象プロジェクト参照に加える




  6. Nunitライブラリを参照に加える
    TDDをインストールしたディレクトリに配置されている。


  7. テストクラスを実装する
    Message.GetMessage の戻り値が「Hello」であるか検証するコード


  8. テストを走行する
    コンテキストメニューの「Run Test」はカーソル中テストのみを実行する


  9. テスト走行結果を確認する
    コンソールに結果が出力される。
    今回は失敗したので、1 failed


  10. Nunit上でテスト走行させる
    今回は失敗したので、Red






  11. テスト仕様に基づいて、実装コードを修正する


  12. コンテキストから再テスト走行
    今回は成功したので、1 pass


  13. Nunitで再テスト走行
    今回は成功したので、Green
  14. 「3」ないし「7」を繰り返す