投了GE的Summer

    学校用Bras很慢,直接上到远程服务器上网申。居然没有OQ,改了改大摩的cover letter,半个小时搞定。GE的网申系统的牛B之处在于,可以从你提交的CV里取出你的Work Experience(虽然他把我的Education Background给当成了Work Exp了)。其他问题也不多。

    上次的微软估计没戏了,过了一个半月了;大摩上次说还没有计划开始。

    P.S. 博客最近都是“阳春白雪”的stuff,搞点“下里巴人”的。嗯,突然想起了高中的生物老头郑家骥郑老师,课上突然冒出这一句把大伙儿雷得不行。

Selenium, Eclipse, Junit折腾记

    很早以前就想搞自动化Web功能测试,知道了Selenium,看了些文档,但当真正到开发项目的关头,测试总是草草而过,跑完一遍手工的拉倒,回归测试更是无从谈起。前几天终于痛下决心写起使用Selenium的Web自动测试代码。

    先扯扯Selenium(字面上是“硒”的意思)。当初出来的时候结结实实把我震撼了一回。原来搞Web自动化测试基本上走的是GUI的那条老路(当然可能也是我当年孤陋寡闻),而这种GUI自动测试工具往往是功能强大的私有软件(比如WinRunner),另外对Web这种多变的测试元素用起来也是很别扭。Selenium另辟蹊径,从JS入手调用浏览器,同时允许通过跨平台的代码调用。从API就可以看出来这个东西的直观易用:

   1:  selenium.windowMaximize();
   2:  selenium.click("link=信息系统");
   3:  selenium.waitForPageToLoad("30000");
   4:  selenium.click("//a[contains(@href, 'projects.do?method:view&project.id=1')]");
   5:  selenium.waitForPageToLoad("30000");

    另外,Selenium还提供了一个Firefox插件-Selenium IDE,用于录制用户的操作(虽然部分动作无法录制)。录制的动作可以直接导出成HTML/Java/Ruby/C#/PHP等格式的代码,配合提供的SeleneseTestCase,当作JUnit的TestCase使用。

    不过这折腾就折腾在这TestCase上。Selenium的开发提供的SeleneseTestCase是Junit3风格的,放在JUnit4底下跑,JUnit4的Annotation功能就用不起来了(这点经我浏览代码查证)。Selenium要启动浏览器,如果用不上@BeforeClass的话,每次启动都初始化一下Selenium,开个IE或者Firefox,这个测试的效率可吃不消(也有比较麻烦的Workaround,但总觉得不是很好)。而甩开SeleneseTestCase的话,又舍不得那个在测试没有通过的时候自动截屏的功能。于是开始Google,兼看Junit4的源代码。

    最后终于在这里找到了方案。但问题又来了:Eclipse自带的Junit 4.3还没有这个方案需要的类(JUnit4ClassRunner )!自己手动把原来的Library移除换上Junit 4.5,又发现这个类才用了一个版本就Deprecated了。于是换上了新类(BlockJUnit4ClassRunner)。

    接下去的工作就是用上Decorator模式,把原来SeleneseTestCase的代码给移到新的BaseTestCase上。期间还遇上了一些Override的问题。上代码:

SeleniumTestListner类,用于拦截异常的抛出

   1:  public class SeleniumTestListener extends RunListener {
   2:   private SeleneseTestCaseAdapter stca;
   3:      @Override
   4:   public void testFailure(Failure failure) throws Exception{
   5:          Selenium selenium = stca.getSeleniumTestBase().getSelenium();
   6:   if(!stca.isCaptureScreenShotOnFailure()){
   7:   return;
   8:          }
   9:   if (selenium != null) {
  10:              String filename = failure.getDescription().getDisplayName() + ".png";
  11:   try {
  12:                  selenium.captureScreenshot(filename);
  13:                  System.err.println("Saved screenshot " + filename);
  14:              } catch (Exception e) {
  15:                  System.err.println("Couldn't save screenshot " + filename + ": " + e.getMessage());
  16:                  e.printStackTrace();
  17:              }
  18:          }
  19:   
  20:      }
  21:   
  22:   public void setSeleneseTestCaseAdapter(SeleneseTestCaseAdapter stca){
  23:   this.stca = stca;
  24:      }
  25:  }
 
SeleniumTestRunner类:加入SeleniumTestListener监听器,得到Test实例并注入监听器
   1:  public class SeleniumTestRunner extends BlockJUnit4ClassRunner  {
   2:   private SeleniumTestListener stl;
   3:   public SeleniumTestRunner(Class<?> c) throws Exception{
   4:          super(c);
   5:          stl = new SeleniumTestListener();
   6:      }
   7:   
   8:      @Override
   9:   public void run(RunNotifier rn){
  10:          rn.addListener(stl);
  11:          super.run(rn);
  12:      }
  13:   
  14:   /**
  15:       * Copy from BlockJUnit4ClassRunner.methodBlock(FrameworkMethod method)
  16:       * to get tested instance
  17:       * @author Marshall
  18:       */
  19:      @Override
  20:   protected Statement methodBlock(FrameworkMethod method) {
  21:          Object test;
  22:   try {
  23:              test= new ReflectiveCallable() {
  24:                  @Override
  25:   protected Object runReflectiveCall() throws Throwable {
  26:   return createTest();
  27:                  }
  28:              }.run();
  29:          } catch (Throwable e) {
  30:   return new Fail(e);
  31:          }
  32:   
  33:   //Marshall added
  34:          stl.setSeleneseTestCaseAdapter((SeleneseTestCaseAdapter)test);
  35:   
  36:          Statement statement= methodInvoker(method, test);
  37:          statement= possiblyExpectingExceptions(method, test, statement);
  38:          statement= withPotentialTimeout(method, test, statement);
  39:          statement= withBefores(method, test, statement);
  40:          statement= withAfters(method, test, statement);
  41:   return statement;
  42:      }
  43:  }

SeleniumTestCaseAdapter, 打上了@RunWith。所有的TestCase都继承这个Adapter。但这个Adapter并不继承JUnit的TestCase类

   1:  /**
   2:   * Decorator pattern which makes this class have the same capability as the
   3:   * SeleneseTestCase class had provided. Copy a lot of source code from the 
   4:   * decorated class.
   5:   * @author Marshall
   6:   */
   7:  @RunWith(SeleniumTestRunner.class)
   8:  public class SeleneseTestCaseAdapter {
   9:   private static SeleniumTestBase stb = new SeleniumTestBase();
  10:   private boolean isCaptureScreenShotOnFailure = false;
  11:   
  12:   /** Use this object to run all of your selenium tests */
  13:   protected static Selenium selenium;
  14:   
  15:      @BeforeClass
  16:   public static void setUpSelenium() throws Exception{
  17:          stb.setUp("http://127.0.0.1:8080/", "*iexplore");
  18:          selenium = stb.getSelenium();
  19:      }
  20:   
  21:      @AfterClass
  22:   public static void tearDownSelenium() throws Exception{
  23:          stb.tearDown();
  24:      }
  25:   
  26:     ......
  27:  }

样例测试类:

   1:  public class TestRiskRepo extends SeleneseTestCaseAdapter {
   2:   public TestRiskRepo(){
   3:          setCaptureScreenShotOnFailure(true);
   4:      }
   5:      @Before
   6:   public void set() throws Exception {
   7:          selenium.open("/apis/login.do");
   8:          selenium.type("j_username", "marshall");
   9:          selenium.type("j_password", "xxxx");
  10:          selenium.click("//input[@value='登录']");
  11:          selenium.waitForPageToLoad("30000");
  12:      }
  13:   
  14:      @Test
  15:   public void repo() throws Exception {
  16:          selenium.windowMaximize();
  17:          selenium.click("link=信息系统");
  18:          selenium.waitForPageToLoad("30000");
  19:          selenium.click("//a[contains(@href, 'projects.do?method:view&project.id=1')]");
  20:          selenium.waitForPageToLoad("30000");
  21:          verifyTrue(selenium.isTextPresent("xxx"));
  22:          selenium.click("link=组织风险库");
  23:          selenium.waitForPageToLoad("30000");
  24:          verifyEquals("组织风险库 | APIS", selenium.getTitle());
  25:          verifyTrue(selenium.isTextPresent("可能性"));
  26:          verifyFalse(selenium.isVisible("//button[contains(text(), '搜索')]"));
  27:          selenium.click("css=.x-tool");
  28:          verifyTrue(selenium.isVisible("//button[contains(text(), '搜索')]"));
  29:      }
  30:   
  31:  }

多数据库SaaS尝试

    手上的APIS要上SaaS,其实就是ASP(Application Service Provider)。要求一份程序,一或多份数据库跑服务。设计的时候参考了阿里软件人写的《互联网时代的软件革命-SaaS架构设计》。这本书从广度上覆盖了做SaaS的很多内容,但深度却显不足,很多地方只是浅尝辄止,一笔带过。

    虽然如此,作为算是这个领域的第一本入门书,还是有一些参考价值的。比如,定义了SaaS四个成熟度模型:定制开发、可配置、高性能的多租户架构、可伸缩的多租户架构;APIS只有一个运行实例,但租户数量不会很多,短期内不会超过50,所以算入第3级,高性能的多租户架构。

    在数据库设计上,这本书对SaaS应用,提供了四个可选方案。独立数据库、共享数据库+隔离数据架构(Schema)、共享数据库+共享数据架构(Schema)。由于采用了MySQL,没有schema的概念,所以去掉了中间的选项。虽然技术人员有上高精尖技术的冲动,不过鉴于我们的用户数不会很多,用户和用户之间也完全隔离,采用了第一种方案。而这种方案,对开发其他功能完全没有任何影响,算是非常unobstructive(中文不好翻译,一般叫“无侵入”)的方案。只要在入口的Filter留个ThreadLocal标志,最底层的SessionFactory做个拦截,功能上算是搞定了。大概就是事先准备好几个SessionFactory,然后哪个用户上来了就给谁相应的SessionFactory。

    但革命的道路是曲折的,过程是漫长的。虽然前期通过阅读Spring代码做过一些技术调查,但Spring+Hibernate这对组合的复杂性还是折腾我老半天。Spring对Hibernate支持很好,几乎是全方位的,这也造成了我要到处扩展用到的Spring支持类。本来只打算对LocalSessionFactoryBean动手,却忘了spring大部分的Bean都是singleton,但又舍不得换成prototype后的性能下降,于是抡起袖子又扩展了BasicDataSource(DBCP)、HibernateTemplate、HibernateDaoSupport、HibernateTransactionManager。好不容易有点眉目,开始从配置的租户数据库载入数据,却屡屡抛session提前关闭的异常。而且诡异的是,这个异常出现的概率约为75%,而一旦我用Debug跟进去后,反而基本不抛异常了。这让我异常胸闷,要知道这种问题十有八九是并发的原因,这问题找起来很头大,往往陷入Spring+Hibernate的汪洋大海。

    接下来几天感冒发烧,好好休息了一阵子,思路却打开了。从网上重新搜出一堆资料。这个是Stack Overflow的一个问答,几个回答都比较靠谱;这个是通过HotSwappableTargetSource提供的DataSource动态替换;这个是Spring开发人员利用AbstractRoutingDataSource提供对DataSource的路由。权衡再三,决定使用AbstractRoutingDataSource。只扩展了两个类就成功实现功能,不抛异常。全局一个SessionFactory,后面几个DataSource轮流上。

    但问题又来了。二级缓存这个东西和SessionFactory绑定,多个DataSource要求要多个Cache,因为各个租户之间的数据主键很可能重复。我试图搜寻通过改造SessionFactory的CacheProvider来搞定,但未果,SessionFactory的Cache埋得有点深。

   最后,参考了这篇这篇,用一个简单的Decorator,退回了多SessionFactory,虽然代码不是很好看,但再无异常。今天收工。

“Internet Explorer 无法打开 Internet站点已终止操作”问题

    以前偶尔被这个问题困扰,总是不知不觉就解决了,这次留个记录。

    先贴个Reference

    症状:打开页面,渲染到一半,弹出对话框,内容如题。点击确定后转到IE自己的错误页面,一点错误信息都没有。Google一番后,得出几个可能性:1、Debugger插件作怪;2、Javascript引号没匹配好;3、在页面渲染期间进行了insertChild操作。

    首先禁用所有调试插件,并设置禁用调试。无效。调出Multiple IE用IE6访问,问题依旧,排除第一条。

    第二人肉扫描代码,没发现问题。Firefox和Chrome页面均正常,基本排除第二条。

    第三条就看Ext.onReady()的位置了,发现了问题。在代码的最后用Ext.onReady()包裹了grid的render调用,但这不够。需要把整个grid的创建过程给embrace(不知道中文用啥好)起来,因为在GridPanel的constructor里就开始渲染了(没有设置lazy-render)。

VC7项目文件转换为VC6文件

    今天研究Notepad++的插件,下了一个demo project下来运行,没想到是vs2003的项目,而我机器上只有vc6。实在不想安装VS200X,体积大速度慢。在网上google了一番发现了一个转换工具(需要注册才能下载,还提供源码)。很方便的命令行工具,直接把sln+vcprojc转成了dsp+dsw。

推荐一款MySQL管理工具

    最近一边进行着apis的开发,一边维护着两个apis实例的运行。以前没有多少维护系统运行的经验,有的也是失败的经验(某一阶段的VOD)。

    维护应用最重要的地方就是系统的数据了。系统崩溃了可以重搭,出Bug了可以改,可要是数据丢失那可就欲哭无泪了。另外,本地开发时往往也需要一些当前product数据库里的数据。为了解决第一个问题,我使用了cron+bash,每个小时给数据库备份一次,并压缩。虽然频率高了点,但是后来证明完全必要(有一次不小心把数据库给干掉了)。

    平时使用MySQL一般用phpMyAdmin,基本上是MySQL居家旅行的管理良药。桌面的几个软件都不怎么用得惯,而且不能管服务器。但是phpMyAdmin的备份和恢复的功能一直都不太好用,经常会超时或者导入错误。今天看到月光博客上的推荐的mysqldumper,马上下载下来装上,发现很好用,备份过程中使用了一些AJAX技术,用户体验很好。上载到服务器上,发现美中不足的是,没有密码限制访问!只好用Require valid-user来手动配置,搞定。

Firefox实现text-overflow:ellipsis

用ExtJS做UI层的确很好看,在IE7(我们抛弃了IE6), Chrome,
Safari下运行都挺好,但Firefox总有点问题,都是关于Grid的。昨天碰到的问题是GridPanel(就是表格),当某列的内容超出了预设的宽度的时候,会把这一行撑爆,结果列就无法对齐,很难看。用Firebug+IEDevBar看了半天(IE是好的,Chrome还没有可用的调试器),才发现问题在于Firefox对text-overflow:ellipsis这个CSS3的属性不支持。Google了一番,这里推荐了两个方法,第一个方法更优雅一些,使用了mozilla的扩展属性-moz-binding,直接用上了XUL和XBL。第二种牵涉到了Javascript,感觉不是很好。这里有一个代码的下载,一开始我自己拷贝代码下来捣持了半天也没搞定,直接下代码就OK。
今天又碰到一个ExtJS在Firefox下显示的问题。有关于GroupingView的GridPanel的。不改了,应该没人会注意到吧。