死拍照的 · 影像志
A Personal Archive · sipaizhao.de

死拍照的.

SIPAIZHAO · SINCE 2012
篇目38
影像42
年份03
最近2014
2012
No.012012.03.18周日16:069:00-18:00

使用Selenium RC侦测页面的JavaScript错误

SeleniumJavaScriptTesting

之前有同事问起Selenium是否可以揪出页面的JavaScript错误,看了下似乎是没有现成的,想想确实有必要做,列入计划却一直没做。直至有一次发布完冒烟却发现个历史遗留的JavaScript错误,想来很恐惧,这么多页面心里没底也不知道还遗留了多少,赶紧将此事提上日程把事儿给办了,免得后患无穷。

还是用的User Extension的办法,具体实施细节可以参考:Selenium User Extension。Selenium的User Extension很强大啊,可以自由扩展功能。

放些代码,与人方便:)

user-extensions.js文件,这里面调用了Firefox的XPCOM,获取控制台的JavaScript错误。下面介绍三个函数,后面会在client driver里调用这三个函数。这三个函数的作用分别如下:

1、doBeginJsErrorChecker : Registers a listener for notification when an error is logged。

2、getJSErrorsChecker : error会存在一个全局变量里,执行这个函数时返回所有的error。

3、doEndJsErrorChecker : Unregisters a listener。


// ==================================================
// Report Javascript Errors using Selenium Exceptions 
// ================================================== 
// Courtesy of Jerry Qian (http://sejq.blogspot.com/) 
// http://clearspace.openqa.org/message/52135 
// http://jira.openqa.org/browse/SEL-613 
// 
// Adapted to work outside of the Selenium IDE: 
// 
// https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIConsole... 
// https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIScriptE... 
// https://developer.mozilla.org/en/Setting_up_extension_development_env... 
if (browserVersion.isChrome) { 
	var JSErrors = new Array(); 
	var theConsoleListener = { 
		observe:function( aMessage ){//async! 
			if(aMessage instanceof Components.interfaces.nsIScriptError && 
					aMessage.flags != Components.interfaces.nsIScriptError.warningFlag){ 
				JSErrors.push(aMessage.message); 
			} 
		}, 
		QueryInterface: function (iid) { 
			if (!iid.equals(Components.interfaces.nsIConsoleListener) && 
					!iid.equals(Components.interfaces.nsISupports)) { 
				throw Components.results.NS_ERROR_NO_INTERFACE; 
			} 
			return this; 
		} 
	}; 
} 

Selenium.prototype.getJSErrorsChecker = function(){
var errors = "";
if (browserVersion.isChrome) {
for (var i=0;i<JSErrors.length;i++)
errors += "Error: [" + JSErrors[i] + "];";
}
else {
throw new SeleniumError("TODO: Non-FF browser...");
}
return errors;
};

Selenium.prototype.doBeginJsErrorChecker = function(){
JSErrors = new Array();
try {
if (browserVersion.isChrome) {// firefox
var aConsoleService = Components.classes["@mozilla.org/consoleservice;1"]
.getService(Components.interfaces.nsIConsoleService);
aConsoleService.registerListener(theConsoleListener);
aConsoleService.reset();
}
else{
throw new SeleniumError("TODO: Non-FF browser...");
}
}
catch (e) {
throw new SeleniumError("Threw an exception: " + e.message);
}
};

Selenium.prototype.doEndJsErrorChecker = function(){
try {
if (browserVersion.isChrome) {// firefox
var aConsoleService = Components.classes["@mozilla.org/consoleservice;1"]
.getService(Components.interfaces.nsIConsoleService);
aConsoleService.unregisterListener(theConsoleListener);
aConsoleService.reset();
}
else{
throw new SeleniumError("TODO: Non-FF browser...");
}
}
catch (e) {
throw new SeleniumError("Threw an exception: " + e.message);
}
};

运行rc时,指定userExtensions文件。之前用selenium server 0.9.2版本的,在firefox里总是有个JavaScript错误:"setting a property that has only a getter",结果一堆误报。Google了一下发觉也有其他人遇到过,更新到最新版本的selenium-server-standalone-2.4.0.jar就好了。


java -jar yourdir\selenium-server-standalone-2.4.0.jar -userExtensions yourdir\user-extensions.js

client driver的代码,改写selenium源文件也是可以的,不过这样不方便维护,可以写一个类继承DefaultSelenium。需要注意的是在user-extensions.js中,函数的命名如果是以do开头的,比如doBeginJsErrorChecker,那么在调用时需要写成beginJsErrorChecker。如果是以get开头的,比如getJSErrorsChecker,那么调用的时候就直接getJSErrorsChecker。

Actions are named starting with “do”, while accessors are named starting with “get”.
You can see in selenium-commandhandlers.js that the two are registered differently (look for _registerAllAccessors compared to _registerAllActions).


	public void beginJsErrorChecker() {
		commandProcessor.doCommand("beginJsErrorChecker", new String[] {});
	}

public String getJsError() {
return commandProcessor.doCommand("getJSErrorsChecker", new String[] {});
}

public void endJsErrorChecker() {
commandProcessor.doCommand("endJsErrorChecker", new String[] {});
}

具体的case中,在case开始的时候beginJsErrorChecker,case执行结束后getJsError获取控制台的JavaScript错误,并endJsErrorChecker。


        @Test
        public void test_jsErrorCheck() {
        	selenium.beginJsErrorChecker();
	        selenium.open("/");
	        //具体的操作
	        String jsError = selenium.getJsError();
	        assertTrue("JavaScript Error: "+jsError,"OK,".equals(jsError));
	        selenium.endJsErrorChecker();
        }

可以想到每个case都要做这样的初始化操作,利用Junit的Setup和tearDown的话就行了(其实不必每个testcase都做初始化,只要把那个记录error的全局变量给清空就行,看自己需要吧)。我在这里做了一件傻事情,在endJsErrorChecker之前先执行assert,结果assert失败(也即是页面有Js错误)时,后面那句endJsErrorChecker没有执行,再次beginJsErrorChecker时就悲剧了,导致后面的case全出问题了。多亏师兄帮我找到原因n_n


    public void setUp() {
        selenium.beginJsErrorChecker();
    }

public void tearDown() {
String jsError = selenium.getJsError();
selenium.endJsErrorChecker();
assertTrue("JavaScript Error: "+jsError,"OK,".equals(jsError));
}

运行了两百多个testcase,发现两个很隐蔽的bug,心里暗爽,今后终于可以放心优化JS代码了!

No.022012.03.18周日16:239:00-18:00

正则表达式入门

Regex

正则表达式者,就是用一个“字符串”来描述一个特征,然后去验证另一个“字符串”是否符合这个特征,达到查找或替换的目的。

(一)正则表达式规则:

1) 匹配位置
举些最常用的例子:^和$是表示字符串首尾位置的正则表达式, \b用来表示单词边界(所谓单词边界指的是:字母/数字/下划线的字符A旁边的字符B非字母/数字/下划线,A、B两个字符之间的位置)

2) 匹配字符
就如.一般能匹配除了换行符(\n)以外的任意一个字符,\d可匹配任一数字,\w可匹配任一字母数字或下划线,\s则可以匹配包括空格、制表符、换页符等空白字符的其中任意一个

3) 字符集合
[ab] 可以匹配字符a或字符b
[A-z] 在ascii字符中位置在A到z之间的字符都可以匹配上
[^ab] 可以匹配非a且非b的字符

4) 子表达式
( ) 括号中的表达式可以作为整体被修饰,另外取匹配结果的时候,括号中的表达式匹配到的内容可以被单独得到(引用)
| 左右两边表达式之间 "或" 关系,匹配左边或者右边。
(query|search) 可以匹配query或search

5) 量词
?表示前面的内容(字符、字符集合或者子表达式)出现零个或一个
+表示前面的内容(字符、字符集合或者子表达式)出现一个或多个
*表示前面的内容(字符、字符集合或者子表达式)出现零个或多个
{n}表示前面的内容(字符、字符集合或者子表达式)出现n个
{n.m}表示前面的内容(字符、字符集合或者子表达式)出现n到m个
{n,} 表示前面的内容(字符、字符集合或者子表达式)出现>=n个
{,n}表示前面的内容(字符、字符集合或者子表达式)出现<=n个

6) 前后查找
向前查找(肯定顺序环视):(?=)
向后查找(肯定逆序环视):(?<=)
负向前查找(否定顺序环视):(?!)
负向后查找(否定逆序环视):(?< !)
与4)不同的是前后查找都是非捕获型括号,没有将值存入变量。比如下面这个例子,匹配出>与<之间的数字

echo '<docitem key="uid">393500994</docitem>' | grep -P "(?<=>)[0-9]{1,9}(?=<)" -o

以上1~5的内容用过正则表达式的都会比较熟悉,但是使用时候可能还是不清楚哪些是所谓的“特殊字符”,了解每个元字符的含义就知道什么时候需要转义
匹配位置的^$、转义符\、匹配除换行外任一字符的.、表示字符集合的[](以及在[]中的^-)、表示子表达式的() |、表示量词的?+*{}(以及在{}中的,)
需要注意,在[]中只有^-\三种元字符,因为其他字符在字符集合中没有特殊含义,但转义了也没坏处。当^不在最前、-不在中间也不需要转义,因为此时他们也没有任何特殊含义。
概括起来说,元字符就是这些:

.^$\[^-\] (|)*+?{,}

举例说明,这是以前测试过的一段有问题的js代码,想用正则表达式来实现一段文本中匹配词加粗的功能


function mytest(str, key, hlTempl) {
        // 特殊正则字符
        var special = /[\\\/?!*-+^\[\]\(\)]/ig;
        // 过滤,加上try/catch避免意外
        try {
                key = key.split(/\s+/);
                for (var i = 0, l = key.length; i < l; i++) {
                        // 单个特殊字符直接跳走,还要处理空字符,否则会多一次循环
                        if(special.test(key[i]) || key[i] === '') continue;
                        // 特殊字符转义
                        key[i] = key[i].replace(special, function(m){return "\\" + m});
                        str = str.replace(new RegExp('(' + key[i] + ')', 'ig'), hlTempl);
                }
        } catch(e) {}
        alert(str);
}

参数中的str表示文本,key表示要匹配加粗的词(如果有多个则用空格分隔),hlTempl是字符串“$1”,用于加粗。
代码不复杂,不细说了。
测试一下这段代码:

var special = /[\\\/?!*-+^\[\]\(\)]/ig;
alert(special.test("-"));

可以发现,“单个特殊字符直接跳走”的这句代码中,试图标记为特殊字符的“-”没有被当作特殊字符处理,原因是在字符集合[]中,-被当作连接符来用了,[\\\/?!*-+^\[\]\(\)]中的*-+这段表示从*到+的ascii字符。

另外字符/和!都不是元字符,不需要出现在这个正则中,而()在字符集合中是不需要转义的。根据这段正则想表达的意思推测,正确的正则表达式应该是[\\/?!*\-+^\[\]()]。
再对照先前列出的原字符 ^.\[^-\]*+?{,}$(|),还有几个元字符没有考虑到:
.$|{,}

mytest("adobe^.^",".","<strong>$1</strong>")
mytest("adobe","$","<strong>$1</strong>")
mytest("adobe","|","<strong>$1</strong>")
mytest("adobe","a{1}","<strong>$1</strong>")

要再补上这几个,那段正则才能成事。

ps. 有个与正则无关的问题:这段代码里有一个循环导致的bug,替换完了的字符又被替换了一遍,将<strong></strong>中的字符再一次替换了~

mytest("<adobe>","< <","<strong>$1</strong>")
No.032012.03.19周一10:239:00-18:00

iOS的自动化测试

iOSTestingAutomation

去年5月份接手测试了几个iOS客户端,简单看了点关于iOS UI自动化测试的东西,做了点记录,现在看看已经落伍了好多。姑且放着也许哪天还能用上。

要测试一个已成型的应用,从用户所见的角度来做自动化收益还是比较高的。目前了解的UI测试方法分为两类,一种是iOS4提供的UI Automation,一种是把测试代码注入到应用中。

1) iOS4的UI Automation

用JavaScript驱动在应用上模拟用户行为,由Instruments的Automation工具执行。具体的可以参考这篇文章在iOS 4 中实现UI自动测试,操作很简单,先编写自动化测试的Javascript文件,在Automation工具中选择这个文件,选择测试的target(模拟器和真机都可以),然后点Record(这个名字起得很坑爹,我一度以为它支持录制,像Selenium一样转化为js代码呢),此时会运行所选的应用同时自动化脚本也开始运作了。

API可以在SDK Developer Document里找到,主要的是UIAElement、UIAElementArray、UIALogger这几个。但是API不是很完善,比如我要得到整个elementTree可以通过UIATarget.localTarget().logElementTree()得到,但是没有API能获取所有的Element,获取Element只能以获取子控件的形式一级一级查找,最后的代码可能就会变成这样:

window.tableViews()[0].cells()[1].buttons()[2].tap();

即使可以通过button的name直接找到这个button也需要写成这样:

window.tableViews()[0].cells()[1].buttons().firstWithName("search");

非常难看难维护。我尝试遍历一个view上的所有控件整整运行了两分钟。

另外推荐一个测试框架,Working with UIAutomation这篇文章中提供了tuneup_js这样一个框架,封装得非常简单,除了没有before after之类的封装外对我来说暂时已经够用了(需要每个case执行完后或者执行开始前恢复默认状态,不过这个很容易实现),可以参考。

2) 测试代码注入到应用代码中

大致的思路是,新建一个测试的target,在applicationDidFinishLaunching最后创建一个测试对象,这个对象封装在测试的代码中,那么此时这个target就是应用+测试的新的东西了,安装后可以看到应用一直在模拟用户行为,也就是测试代码在运行。

这种测试方法其他部门的同事在研究,这里可以介绍几个测试框架:

FoneMonkey,这是我最早接触的iOS自动化框架,支持录制回放,但是不知道怎么对结果做验证。如果仅仅是录制回放的话,UI Recorder已经挺好用了。

Bromine,这个框架还不错,封装到最后只需要填几个Plist就可以完成testcase,只是不方便扩展,可以模拟用户行为无法做数据验证,同事基于这个框架在做定制,想法是做成C/S模式,这样如果server端没有发送请求测试就不会进行。

Google Toolbox for Mac (GTM),Google的一个开源项目。GTM + TestMerge.app = UI testing bliss据说也是类似的思路。

总结一下以上两种测试方法存在的问题:

iOS4的UI Automation有一个硬伤,就是4.0以下iOS不支持,这对自动化来说是打点折扣的。但是既然是Instruments的工具,不知道能否和其他工具一起使用,比如用leak检测内存泄露,比如用UI Recorder记录操作,然后回放到低版本的iOS设备或者模拟器上,可行性没了解过。

第一种方案使用Javascript,相对第二种方案的Objective C上手还是要简单一些。

需要解决的问题还有,如果应用crash,测试就不能继续了。如果crash后重跑下一个case,那就不能有case之间的耦合。如何重新运行app有待研究。

另外以上两种方案最后都要做到可持续集成,第一种方案需要做的是把build app、run app & testcase、generate testresult整个流程串起来,Automation这个工具提供可以测试报告,Instruments可以Shell运行,是否可行还需要研究,如果行不通的话可以尝试用Apple Script运行;第二种方案难点在于如何生成报告,需要把测试的log重定向到某个文件输出,这也是他们准备做成C/S结构的原因之一,可以在server端直接得到测试结果。

PS:如果测试的不是客户端而是web应用的话,Selenium2已经支持iOS和android平台了,可参考Selenium IphoneDriver

No.042012.03.19周一10:3118:00-09:00

Yupoo for Wordpress 插件开发

YupooWordPressPlugin

某人的flickr相册很久没有更新了,于是想把他博客上的flickr换成我的Yupoo,Yupoo官方没有提供WP插件,搜了一把也没有现成合适的,只好自己写一个简单的widget插件。网上找了几个不错的中文教程,捣鼓了几下终于搞定了,感觉WP widget插件开发还是非常简单的,以后博客我可以随便折腾了~

Hello, world

WP插件的目录结构很简单,一般在wp-content\plugins目录下建立一个目录,目录的名字取插件的名字,里面放一个同名的php文件即可。以后提交到官网时会需要一个readme.txt以及截图,开发的时候可以先不加。当然也可以是在本地先写好再打包成zip上传,最后也会是在wp-content\plugins目录下的。

一个什么功能都还没实现的插件也必须要有的就是插件概要信息。下面这段代码注释那部分就是概要信息,如果概要信息都填错了,安装插件时会报错。


<?php
/*
Plugin Name: test
Plugin URI: http://fuzhijie.me
Description: test
Version: 0.0.1
Author: fine
Author URI: http://gagharv.yupoo.com
License: GPL
*/

//下面开始是插件实现的代码
echo "Hello,World";
?>

Hooks

上面这样一个插件激活后会在所有页面上都输出一个"Hello,World",一般都是在特定情况下才会执行某些函数的,所以就用到了hooks,在某个切入点时执行我们的代码。


add_action ($hookname, $callbackfunction)
add_filter ($hookname, $callbackfunction)

个人感觉应该add_filter是针对内容的,add_action是针对行为的。参考下Wordpress官方API文档Action Reference以及Filter Reference就比较清楚了。比如我们要在模板调用wp_head这个函数时才输出"Hello,World",那么只要改为下段代码即可。


function helloworld() {
    echo "Hello,World";
}

add_action('wp_head', helloworld);

Settings

以上只是最简单的范例,主要功能的实现就不再赘述。下面入正题,看看我们插件是如何在设置里面加入相关的设置菜单和设置项,最终会显示在管理界面左边Settings/设置的地方。


if( is_admin() ) {
    /*  利用 admin_menu 钩子,添加菜单 */
    add_action('admin_menu', 'display_yupoo_menu');
}

function display_yupoo_menu() {
/* add_options_page( $page_title, $menu_title, $capability, $menu_slug, $function); */
/* 页名称,菜单名称,访问级别,菜单别名,点击该菜单时的回调函数(用以显示设置页面) */
add_options_page('Yupoo Settings', 'Yupoo Settings', 'administrator', 'Set_Yupooer', 'display_yupoo_settings_page');
}

function display_yupoo_settings_page() {
?>
<div>
<strong>Yupoo账号设置</strong>
<form method="post" action="options.php">
<?php /* 下面这行代码用来保存表单中内容到数据库 */ ?>
<?php wp_nonce_field('update-options'); ?>

<p><label for="yupooer">用户名:</label><input id="yupooer" type="text" name="yupooer" value="<?php echo get_option('yupooer'); ?>" /></p>
<p><label for="yupoo_number">显示照片数量:</label><input id="yupoo_number" type="text" name="yupoo_number" value="<?php echo get_option('yupoo_number'); ?>" /></p>
<p><label for="yupoo_width">单张照片宽度:</label><input id="yupoo_width" type="text" name="yupoo_width" value="<?php echo get_option('yupoo_width'); ?>" /></p>
<p><label for="yupoo_height">单张照片高度:</label><input id="yupoo_height" type="text" name="yupoo_height" value="<?php echo get_option('yupoo_height'); ?>" /></p>
<p>
<input type="hidden" name="action" value="update" />
//注意,这里是用来指定哪些字段需要更新,各字段用逗号分开
<input type="hidden" name="page_options" value="yupooer,yupoo_width,yupoo_height,yupoo_number" />
<input type="submit" value="保存设置" class="button-primary" />
</p>
</form>
<div>Yupoo的RSS订阅:http://www.yupoo.com/services/feeds/photos/<strong><?php echo get_option('yupooer') == ''?'yourname':get_option('yupooer'); ?></strong>/</div>
</div>
<?php
}
?>

主要是加个action为options.php的form,后面有三个隐藏的input的是用来提交时更新数据的,page_options是要指定哪些字段需要更新。之后可以通过get_option()方法得到我们保存的数据。

Widget

实现Widget也比较简单,以下是实现Yupoo for WP的Widget部分代码。


class yupoo extends WP_Widget {
    /** 构造函数 */
    function yupoo() {        
        $widget_ops = array(            
            'description' => '将Yupoo拖动到右侧侧边栏即可使用'
        );
        parent::WP_Widget('Yupoo', $name = 'Yupoo Widget',$widget_ops);    
    }

/** 输出widget内容 */
function widget($args, $instance) {
extract($args);
echo $before_widget.$before_title.$instance['title'].$after_title;
display_yupoo_album(); //这个是我自定义输出内容的代码
echo $after_widget;
}
/** widget选项保存过程 */
function update($new_instance, $old_instance) {
return $new_instance;
}

/** 在管理界面输出选项表单(就是在Widget里面拖动时会显示的那个选项,不是在Settings/设置里的) */
function form($instance) {
$title = esc_attr($instance['title']);
?>
<p><label for="<?php echo $this->get_field_id('title'); ?>"><?php _e('Title:'); ?> <input class="widefat" id="<?php echo $this->get_field_id('title'); ?>" name="<?php echo $this->get_field_name('title'); ?>" type="text" value="<?php echo $title; ?>" /></label></p>
<?php
}

}

add_action('widgets_init', create_function('', 'return register_widget("yupoo");'));

最后附上我们的"Hello, World",在widget显示又拍照片的代码,其实就是取了用户又拍的feed解析了一下:)


function display_yupoo_album() {
    $yupooer = get_option('yupooer');
    $width = is_numeric(get_option('yupoo_width'))?get_option('yupoo_width'):80; 
    $height = is_numeric(get_option('yupoo_height'))?get_option('yupoo_height'):80; 
    $number = is_numeric(get_option('yupoo_number'))?get_option('yupoo_number'):9;
    $content = file_get_contents("http://www.yupoo.com/services/feeds/photos/$yupooer/");
    $xml = simplexml_load_string($content);
    $item = $xml->channel->item;
    $count = count($item)>$number?$number:count($item);
    for($i=0; $i<$count; $i++) {
        preg_match('/img src="(.*?)"/', $item[$i]->description, $matches);
        $href = $item[$i]->link;
        echo '<a href="'.$href.'" target="_blank"><img src="'.$matches[1].'" width="'.$width.'" height="'.$height.'" style="margin:1px"></a>';
    }
}

提交插件到Wordpress官网

插件首先要通过wordpress官方认证,这个认证有点意思,一个白天就能搞定,认证也不需要看代码什么的,只要讲下要做什么就行。认证成功之后会有一个svn地址,代码和需要的文件都放在trunk里面,每次发布稳定版本时打一个tag出来。除了代码之外,要添加以下几个文件:

readme.txt,官网有个范例文件验证器,记得用UTF-8编码哦,不然中文会乱码。readme.txt每个段会对应插件的detail里面一个tab。detail中文换行似乎有问题,经过实践发现,每一行后面加一个空行就能正确换行了。

截屏文件,在readme.txt里面有一个Screenshots段可以显示图片,直接在代码目录下存放命名为screenshot-1.png、screenshot-2.png的图片,图片会按照顺序显示出来。在Screenshots段填入以下内容,每一条会对应一张照片。


== Screenshots ==

1. 在侧边栏显式的效果
2. 后台设置界面

参考资料:

1、《WordPress插件开发之Widget侧边栏挂件插件》

2、《怎样开发一个 WordPress 插件》

3、《WordPress插件开发》

4、《如何向 WordPress 官网提交自己做的插件》

代码可以在这里下载到:Yupoo-Plugin

No.052012.03.19周一10:389:00-18:00

截图对比测试

SeleniumTesting

大家知道,Selenium的自动化是比较方便的检查页面是否存在某个元素,也能够模拟点击、按键等一些操作。但是,对于页面上多了一点东西,样式变了一点,是无能为力的。在大量feature需要回归的时候,对比截图的测试提供了一种较为快捷的测试方法。其实就是个图形化的super diff~

基本思路是:使用相同的URL,同时访问测试环境和线上环境,生成两张页面的图片,然后对比这两张图片,看是否相同,相同就pass。不相同就fail,并标明是那个地方不同。Selenium本来就支持页面截图,于是Google "java image comparison library"后,辗转找到了ImageMagick。非常好用。

图中红色的就是对比不一致的地方。大家来找茬:)

解决最重要的对比library之后,剩下的就是外围的封装和自动化。提供一些思路:

1. 测试环境和线上所访问的后台数据必须一致,否则由于数据不一致导致的图形不一致,对比就没有意义了。如何一致,也是比较棘手的事情,当前想到的使用memcached;另一种思路是直接mock一些后端系统。

2. 如何调用ImageMagick?可以直接使用ImageMagick提供的命令行工具,也可以使用它提供的API接口(当前只有C/C++/Perl),因此,如果java想调用的时候,可能需要使用JNI来调用。我们目前是用selenium执行截图的操作,对比则用ImageMagick的命令行方式完成。ImageMagick在对比两张长宽不同的图片时会对比失败,目前我们的处理方式是将长或宽的图用ImageMagick剪裁成与另一张图同样大小的图片后再进行对比。

3. 在运行自动化代码的机器上保存图片、直接得到对比结果
由于截图是截取整个页面,所以常遇到页面上某个需求的小改动(比如改页头)导致截图对比绝大部分fail的情况,在不确定是否是这种需求变更导致的情况下,如果截取的图片保存在rc端,每张图片都还是要远程登录到rc机器上肉眼确认一下的。
但是如果case fail得少就不是很方便了,因为要再跑到rc的机器上去执行比较,diff的结果就不是那么方便就能够取到。其实Selenium除了提供captureEntirePageScreenshot和captureScreenshot外,还提供了captureEntirePageScreenshotToString和captureScreenshotToString,可以直接得到图片的base64编码。只需要自己再做一次解码就能够将图片保存在运行自动化代码的服务器上了。目前我们是用shell将图片做对比后将对比后的图片放到一个web目录下,这样可以直接在自己的PC上通过浏览器看对比结果。
更近一步,想把截图对比做成一个服务,也不是难事。给两个URL就可以直接给出对比结果。

4. 截取自己所需部分的图片
打个比方,需要测试各个应用或者页面的页头部分,那么可能就不关心页面其他的变动,只要求对页头这部分进行对比。目前已经实现但是并没有真正用起来,因为即使这部分的样式是正确的,也无法保证它在页面上出现的位置,这点还是我比较担心的。Anyway,说明一下“截取自己所需部分的图片”在网上看到的两种思路:

1)定位某个元素的位置,然后用js定位到这个坐标。然后captureScreenshot截当前屏幕。但是这不能完全解决问题,只能说是截取的部分变小了。如果需要截图的部分一页还显示不下,那就悲剧了,只能截到一屏的图。这种方式处理起来很简单,但是并不理想。


Number x = selenium.getElementPositionLeft("//div[@id='p4p-sidebar']");
Number y = selenium.getElementPositionTop("//div[@id='p4p-sidebar']");
selenium.getEval ("window.scrollTo("+x+","+y+")");
String pic = selenium.captureScreenshot();

2)用user extension。参考了下这篇文章user-extension.js 之 Firefox 获取可见控件的screenshot,效果非常好,只要locator可以定位到的都可以截图。代码改动也不算大,而且很容易扩展。


Selenium.prototype.doCaptureImage = function(filename,locator) {
    /**
     * Saves the entire contents of the current window canvas to a PNG file.
     * Currently this only works in Mozilla and when running in chrome mode.
     * Contrast this with the captureScreenshot command, which captures the
     * contents of the OS viewport (i.e. whatever is currently being displayed
     * on the monitor), and is implemented in the RC only. Implementation
     * mostly borrowed from the Screengrab! Firefox extension. Please see
     * http://www.screengrab.org for details.
     */
   var kwargs = "FFFFFF";
   var elementLeftX = this.getElementPositionLeft(locator);
   var elementTopY = this.getElementPositionTop(locator);
   var elementWidth = this.getElementWidth(locator);
   var elementHeight = this.getElementHeight(locator);

// can only take screenshots in Mozilla chrome mode or IE (non-PI). But
// since IE support is HIGHLY EXPERIMENTAL, don't advertise it in the doc.
if (!browserVersion.isChrome && !browserVersion.isIE) {
throw new SeleniumError('takeScreenshot is only implemented for '
+ "chrome and iexplore browsers, but the current browser isn't "
+ 'one of them');
}

// do or do not ... there is no try

if (browserVersion.isIE) {
// targeting snapsIE >= 0.2
function getFailureMessage(exceptionMessage) {
var msg = 'Snapsie failed: ';
if (exceptionMessage) {
if (exceptionMessage ==
"Automation server can't create object") {
msg += 'Is it installed? Does it have permission to run '
'as an add-on? See http://snapsie.sourceforge.net/';
}
else {
msg += exceptionMessage;
}
}
else {
msg += 'Undocumented error';
}
return msg;
}

if (typeof(runOptions) != 'undefined' &&
runOptions.isMultiWindowMode() == false) {
// framed mode
try {
new Snapsie().saveSnapshot(filename, 'selenium_myiframe');
}
catch (e) {
throw new SeleniumError(getFailureMessage(e.message));
}
}
else {
// multi-window mode
if (!this.snapsieSrc) {
// XXX - cache snapsie, and capture the screenshot as a
// callback. Definitely a hack, because we may be late taking
// the first screenshot, but saves us from polluting other code
// for now. I wish there were an easier way to get at the
// contents of a referenced script!
var snapsieUrl = (this.browserbot.buttonWindow.location.href)
.replace(/(Test|Remote)Runner\.html/, 'lib/snapsie.js');
var self = this;
new Ajax.Request(snapsieUrl, {
method: 'get'
, onSuccess: function(transport) {
self.snapsieSrc = transport.responseText;
self.doCaptureEntirePageScreenshot(filename, kwargs);
}
});
return;
}
// it's going into a string, so escape the backslashes
filename = filename.replace(/\\/g, '\\\\');

// this is sort of hackish. We insert a script into the document,
// and remove it before anyone notices.
var doc = selenium.browserbot.getDocument();
var script = doc.createElement('script');
var scriptContent = this.snapsieSrc
+ 'try {'
+ ' new Snapsie().saveSnapshot("' + filename + '");'
+ '}'
+ 'catch (e) {'
+ ' document.getElementById("takeScreenshot").failure ='
+ ' e.message;'
+ '}';
script.id = 'takeScreenshot';
script.language = 'javascript';
script.text = scriptContent;
doc.body.appendChild(script);
script.parentNode.removeChild(script);
if (script.failure) {
throw new SeleniumError(getFailureMessage(script.failure));
}
}
return;
}

var grabber = {
prepareCanvas: function(width, height) {
var styleWidth = width + 'px';
var styleHeight = height + 'px';

var grabCanvas = document.getElementById('screenshot_canvas');
if (!grabCanvas) {
// create the canvas
var ns = 'http://www.w3.org/1999/xhtml';
grabCanvas = document.createElementNS(ns, 'html:canvas');
grabCanvas.id = 'screenshot_canvas';
grabCanvas.style.display = 'none';
document.documentElement.appendChild(grabCanvas);
}

grabCanvas.width = width;
grabCanvas.style.width = styleWidth;
grabCanvas.style.maxWidth = styleWidth;
grabCanvas.height = height;
grabCanvas.style.height = styleHeight;
grabCanvas.style.maxHeight = styleHeight;

return grabCanvas;
},

prepareContext: function(canvas, box) {
var context = canvas.getContext('2d');
context.clearRect(box.x, box.y, box.width, box.height);
context.save();
return context;
}
};

var SGNsUtils = {
dataUrlToBinaryInputStream: function(dataUrl) {
var nsIoService = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
var channel = nsIoService
.newChannelFromURI(nsIoService.newURI(dataUrl, null, null));
var binaryInputStream = Components.classes["@mozilla.org/binaryinputstream;1"]
.createInstance(Components.interfaces.nsIBinaryInputStream);

binaryInputStream.setInputStream(channel.open());
return binaryInputStream;
},

newFileOutputStream: function(nsFile) {
var writeFlag = 0x02; // write only
var createFlag = 0x08; // create
var truncateFlag = 0x20; // truncate
var fileOutputStream = Components.classes["@mozilla.org/network/file-output-stream;1"]
.createInstance(Components.interfaces.nsIFileOutputStream);

fileOutputStream.init(nsFile,
writeFlag | createFlag | truncateFlag, 0664, null);
return fileOutputStream;
},

writeBinaryInputStreamToFileOutputStream:
function(binaryInputStream, fileOutputStream) {
var numBytes = binaryInputStream.available();
var bytes = binaryInputStream.readBytes(numBytes);
fileOutputStream.write(bytes, numBytes);
}
};

// compute dimensions
var window = this.browserbot.getCurrentWindow();
var doc = window.document.documentElement;
var box = {
x: elementLeftX,
y: elementTopY,
width: elementWidth,
height: elementHeight
};

LOG.debug('computed dimensions');

var originalBackground = doc.style.background;
/* change background;
if (kwargs) {
var args = parse_kwargs(kwargs);
if (args.background) {
doc.style.background = args.background;
}
}
*/
// grab
var format = 'png';
var canvas = grabber.prepareCanvas(box.width, box.height);
var context = grabber.prepareContext(canvas, box);
context.drawWindow(window, box.x, box.y, box.width, box.height,
'rgb(0, 0, 0)');
context.restore();
var dataUrl = canvas.toDataURL("image/" + format);
LOG.debug('grabbed to canvas');
doc.style.background = originalBackground;

// save to file
var nsFile = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
try {
nsFile.initWithPath(filename);
}
catch (e) {
if (/NS_ERROR_FILE_UNRECOGNIZED_PATH/.test(e.message)) {
// try using the opposite file separator
if (filename.indexOf('/') != -1) {
filename = filename.replace(/\//g, '\\');
}
else {
filename = filename.replace(/\\/g, '/');
}
nsFile.initWithPath(filename);
}
else {
throw e;
}
}
var binaryInputStream = SGNsUtils.dataUrlToBinaryInputStream(dataUrl);
var fileOutputStream = SGNsUtils.newFileOutputStream(nsFile);
SGNsUtils.writeBinaryInputStreamToFileOutputStream(binaryInputStream,
fileOutputStream);
fileOutputStream.close();
LOG.debug('saved to file');
};

No.062012.03.19周一11:2418:00-09:00

JSONP那些事儿

JavaScript

前几日某人突发奇想,想在博客简介的地方随机显示一些小段子,商量了下就打算用饭否上的feed作为数据源。异国的dreamhost服务器访问饭否延迟大,某人担心会影响他苦心经营的博客的用户体验,所以我就把这段代码从服务器端挪到了客户端。最后效果可以访问某人的博客,刷博客可以看段子哦。

既然用JavasScript,那么http://fuzhijie.me怎么访问http://fanfou.com又成了问题,一般的feed都是XML格式的,用AJAX会有跨域问题(必须协议、域名和端口是完全相同的才是“同源”的,否则请求无法成功地取回数据)。于是想到了用JSONP,翻了翻饭否API果然发现饭否的feed还真是有JSON格式的。

倒是一直听说JSONP可以实现跨域,但是究竟什么是JSONP呢?


JSONP = JSON WITH PADDING

搜索了一下这个词的意思也没完全领悟,猜测就是把JSON数据“填充”到某个地方(比如填充到函数的参数部分)。

其实JSONP的原理很简单。因为script的src是不受跨域限制的,比如访问淘宝,会有很多的<script src="http://a.tbcdn.cn/xxx.js"></script>,这就是请求了非taobao域的cdn上的内容,并且这个js中的脚本还会被执行。那么如果是我们博客请求饭否的API也是一样的。


<script src="http://api.fanfou.com/statuses/user_timeline/tar_gz.jsoncallback=json"></script>

这个页面返回的内容形式是这样的:myfunc(myvar),只要在我们博客的页面里已经定义好myfunc这个js函数就可以执行了,而myvar其实就是服务器返回的json数据作为myfunc的参数。


function jsonpHandle(duanzi){ 
	do {
		index = Math.floor(Math.random()*(duanzi.length));
	} while(duanzi[index]["text"].length > 60) //随机取一条长度不大于60的段子
	document.getElementById("description").innerHTML = duanzi[index]["text"];
} 
var JSONP = document.createElement("script") ; 
JSONP.type = "text/javascript"; 
JSONP.src = "http://api.fanfou.com/statuses/user_timeline/tar_gz.json?callback=jsonpHandle"; 

document.getElementsByTagName("head")[0].appendChild(JSONP); //在head之后添加js文件

又看了下淘宝搜索用的JSONP,返回的内容如下所示。这个是把JSON对象赋值给一个变量供js使用,原理也是一样的。


__p4p_etao_sidebar__={"results":"..."}

另外JSONP也是一种JavaScript注入,所以一定要注意安全问题,引用的域要是放心可靠的才可以用哦。

No.072012.03.19周一11:3318:00-09:00

一个人住第一年

父母在,不远游,游必有方

想有个家,一个有归属感的地方。可以下班回家,然后洗衣煮饭。当然青春不能蹉跎给了柴米油盐。我住的地方虽然极不情愿但我只能管它叫宿舍其实我很想有个家

我属猫头鹰,这种计划其实是违背自然规律的。我和自己有很多约定可就像念书时写的学习计划一样既靠谱又不靠谱

我知道我的技术还很糟糕,我还没有把工具变成得心应手的玩具。但是他是我的好朋友~我的心我的眼睛。我没法画出我爱的画面,但是他可以

我记得那个时候心情很糟糕,突然看到花就开心了,窝心又温暖

还是会想起那个半夜企图翻墙去公司,最后买了罐啤酒闷头大醉大睡。倒也没受什么大委屈,可是就会觉得乡音一定会给我很大很大的勇气

这一只脏脏的打补丁的小熊,叫小豆丁。以前从来就没有过毛绒玩具。我真希望在寒冷的冬天里他能给我带来一点温暖

我以前常想,等我离开的时候,我就把这些小纸条给他们看。可是那些人,每个都比我溜得还快!

光芒万丈也好平平淡淡也好,都希望那个乌七八糟的人能开开心心的,也不枉我跟自己怄气怄得要死。我的心等不来你来翻阅

还有太多感受没有拍。《小记》得我快要语无伦次了。师兄说独立了就有归属感了。我不知道这是不是独立,大概就是随遇而安了。现在不论在什么地方都会有种感觉:我难道不就是生活在这里的吗?

2010 i'm fine.

No.082012.03.19周一14:289:00-18:00

一个GBK编码导致的问题

Gbk

前几天微博上看到同事转的一个链接,在soso地图上搜索“泰康金融”会乱码。

@柬之_张光耀:试了一下,“泰康金融”的gbk编码是:%CC%A9%BF%B5%BD%F0%C8%DA,“到”的gbk编码是:%B5%BD,所以杯具的事情发生,“%B5%BD”就匹配上了“%CC%A9%BF%B5%BD%F0%C8%DA”(从第四个位置开始数),因此,就被理解想搜索公交了(关键词中有“到”就被理解成搜索公交路线)

去年的一次线上事故,店铺内搜索个别关键词搜索结果宝贝标题出现乱码,和这个是同一个问题。
现象:店铺内搜索个别关键词搜索结果宝贝标题出现乱码 !如店铺内搜索关键词:男 男鲁。

比如“夏新”的GBK编码是”0xcf 0xc4 0xd0 0xc2″,”男”的GBK编码是”0xc4 0xd0″,引擎在做处理时用strstr来做匹配,结果“男”刚好匹配上了“夏新”中间两个字节,查询时结果错误(搜“男”找到“夏新”的宝贝),然后在宝贝标题标红时出现乱码。

转自引擎组同学的一段话:如果使用UTF-8编码就不会有问题了,因为中文使用UTF-8编码需要三个字节(1110xxxx 10xxxxxx 10xxxxxx),而第一个字节会是’E',后续两个字节都是10开始的,最大也就是’B',这样就不会有错误匹配的问题。

之前在搜索前端也发现过类似的问题,这个bug大概情况是这样的:搜索引擎之前有个bug,q中带有全角或半角空格时,全角/半角的搜索结果不一致,所以在前端紧急fix了一下,前端把q中的全角空格改为半角空格,再传给引擎。


if (strpos($QUERY[$userq]," ")!== false){
    //全角空格
    $QUERY[$userq] = trim(preg_replace("| +|"," ",$QUERY[$userq]));
}

前端当时的做法是在query中查找“%A1%A1”(即全角空格),然后替换为“+”(即半角空格),如果query为“啊 ”(啊加上一个全角空格,也就是“%B0%A1%A1%A1”),替换全角空格为半角空格后为“%B0+%A1”,此时就不是正常的GBK编码了。
PHP中如果要处理多字节字符,就要用mb_***函数。Fix之后的代码:


if (strpos($QUERY[$userq]," ")!== false){
    //全角空格
    mb_regex_encoding("gb2312");//实验时gbk不支持汗
    $QUERY[$userq] = trim(mb_ereg_replace(" +"," ",$QUERY[$userq]));
}

凡是GBK编码,使用一些单字节的字符串处理函数进行查找/替换的时候,貌似都会有这样的问题的。

No.092012.03.19周一16:439:00-18:00

浏览器扩展之HackURL

AutomationBrowserExtensionTool

工作中需要频繁修改测试参数,很麻烦,浪费时间又没技术含量,所以写了一个浏览器扩展,快速加减参数,简直就是解放双手的Web开发测试居家旅行必备之利器。

刚学浏览器扩展时候写的firefox扩展,支持快捷键和即时跳转(比如在菜单栏中选中了debug参数,那么每次刷新、打开页面都会自动带上debug参数),相当好用。
点此下载.xpi文件,用zip解压后可以看源文件。xpi为firefox扩展的扩展名,直接拖到firefox上即可完成安装。

后来小师弟照着写了个chrome版本的,把快捷键和即时跳转去掉了,也就是每次改参数需要点击一下菜单,不过也总比手动输入要快,挺不错的,毕竟我自己现在已经很少用firefox了。坦白说chrome扩展开发比firefox扩展开发要好太多,了解html、css、javascript足矣,不用学xul这样用不到的东西,还不需要重启浏览器,上手飞快。
点此下载.crx文件,直接拖到chrome即可完成安装,用zip解压后可以看源文件。

有了firefox和chrome的自然就有人会问起IE版本的要怎么做呢?如果可以接受像上面提到的chrome版本这样每次都手动点击的话,那是相当简单,都不需要写扩展,并且任何浏览器都适用:
在书签栏中新建一个目录,名字随意。 目录下添加一个书签,名称填入"debug",url填入javascript:window.location.href=window.location.href+"&debug=on"(也可以优化一下这句JavaScript代码~),其他参数可以依葫芦画瓢再加几个书签,要加参数的时候点一下这个书签就像点击菜单一样也很方便。

No.102012.03.23周五16:279:00-18:00

一个检查HTML标签完整性的Chrome扩展

ChromeBrowserExtensionHTMLTool

Web开发经常遇到一些兼容性问题,有很多却是因为HTML标签写错导致的,讨厌就讨厌在有的浏览器会兼容得很好,查半天才发现是标签闭合错误导致。

年前开发的同学在周末的两个连雨天中宅到发霉时想到写了个GreaseMonkey的脚本调用服务器上的tidy来检查HTML标签完整性。她提出的检查标签闭合否,有几个思路:
1. 写正则。不过,html标签不单只有这样<div></div>成对的,还有<input type="text" />这样不必成对的,另外,标签的层层嵌套,让这样的正则恐怕也很难写,网上有些html_parser的例子,但良莠不齐。当然,如果能用正则来实现,那肯定是最快、最通用的办法(欢迎正则大牛们指教)。
2. 利用w3c验证。http://validator.w3.org/ 这里提供了验证网页是否符合w3c标准的页面。但未提供接口。那么我们可以采用服务器端制造post请求的方式向其发出请求得到返回结果,但,它对于汉字编码的支持貌似有些问题,当POST给它的含有中文时,直接就崩溃了。
3. 利用tidy工具。http://tidy.sourceforge.net/ tidy是个开源项目,它也是帮助检查某个文件或某段输入是否符合w3c标准的。源码可以从上述地址中得到,当然它也有打包好的可执行文件,还有PHP\PERL等语言的API。看着像那么回事,行,就是它吧。于是从源码安装了tidy,又写了个php脚本,非常简单。

当时我正好也在尝试用正则找出匹配失败的标签,但是由于规则太过复杂我又想用一个正则解决,导致那个正则表达式到后面越来越复杂,越来越难改,牵一发而动全身,最后无奈搁置了。
W3C的工具感觉检查得太过严格,一堆错误里也不知道哪个才是我真正需要关注的。
在开发同学的推荐下也用了一下tidy,感觉还不错,有错漏的标签还是可以找出来,不过偶尔还是有些问题,比如http://www.nengcha.com/code/url/这个页面</form>和</table>顺序反了的错误就没有检查出来,不知道tidy是怎么检测的。

后来在chrome的webstore里找了找,发现了"HTMLタグ閉じ忘れチェッカー"这个工具,除了script标签以及注释标签中间的会有点误判,还是很好用的,这个问题在介绍里也说明了"※scriptタグやコメントタグ内のタグにも反応してしまうので注意し"。翻了下这个扩展,代码很少,所以花了一个周末看了下。后来连着一两个月陆陆续续改了改修复了些bug,现在已经很好用了:)

主要改动:
1. 多语言支持,加了英文和中文的
2. 修复原先script标签内部的字符被误认为是标签的问题
3. 略过对注释标签中内容的判断,修复了注释标签中内容的误判问题
4. 非已知的HTML标签(比如自定义的标签,像<g:plusone>之类的)输出在控制台了,是否闭合没有做判断
5. 修复原先行号显示undefined的问题
6. 修复刷新页面同时切换tab导致badgeText与当前页面不一致的问题
7. 修复原来localStorage存储空间用完就无法再更新新页面结果的问题

不过仍有个问题,script标签内如果含有标签会判断错误,比如<script type="javascript">document.write('</script>')也会认为是合法的HTML,这个目前无解。试了一下用tidy来检测也有这个问题。echo "<title></title><script type="javascript">document.write('</script>')" | ./tidy 也被认为是正确的HTML。

目前代码托管在Google code,有兴趣的同学可以自取。JavaScript代码也很方便挪到Firefox和Selenium上,简单改动就可以实现了。

https://htmltagchecker.googlecode.com/svn/tags/v1

在chrome中打开chrome://settings/extensions,选择"载入正在开发的扩展工具",选中下载下来的目录即可使用。如果发现bug期待反馈给我:)

原作者已经把最新的代码放到chrome商店了,可以直接通过这个链接安装。
Chrome扩展安装后Windows下所在的目录默认为
C:\Users\用户名\AppData\Local\Google\Chrome\User Data\Default\Extensions

最后谢谢原作者ずま/kzms2 さん带来这么好用的工具,どうもありがとうございます~

No.112012.03.30周五10:3218:00-09:00

彩云之南

No.122012.04.13周五16:5818:00-09:00

长安忆

No.132012.04.13周五17:0818:00-09:00

花重锦官城

我脑海里的成都总是一副悠闲自在的古城模样,几间茶铺,几个麻将桌,几十年。

在成都待了三天,绝大多数时间都是绕着川大的,吃在川大,玩在川大,住也在川大。这是我第一次见到一个大学能这么大,能有小区住人,能有附属幼儿园、小学、中学,还能被其中一个中学戏称为附属大学。学校南门有个角落总是站着几个人,那是一个没有被中介搅局的川大房屋租赁市场,我就在那找了间屋子睡觉,一天四五十块钱,不怎么干净但也能将就,好歹用卫生间只用锁一边的门,然后可以伴着窗外的麻将声睡个午觉。听人说成都是在飞机上就能看见别人打麻将的城市,我在川大倒是没见到,只是散步时会路过些老房子听到哪户人家家里传出来的洗牌声。

川大的四个半门里,三个半门口都能找到好吃的。想来觉得奇怪,从这个被干锅火锅串串包围的学校出来的,我认得的大都是瘦子。这里被人惦记的火锅店实在太多了,想把学校附近的美味尝个遍,三天九顿都吃不完。幸好我虽然喜欢吃辣却又吃不得辣,嘴也就不敢那么贪了,有个晚上去点了份鸳鸯火锅,上了满满一锅红油,却是中间一小撮清汤锅底,想必是四川人用来涮辣味的汤,真是开了眼界。

吃饱了可以去川大的操场晃荡。操场上很热闹,可以学学武术跳跳舞,或者只是简单得绕着走上一二三四圈,和知心友人聊聊未来,和夏夜晚风说说心事。

说说川大遇到的人。南园宿舍的理发店阿姨,她的手艺告诉我为什么这些川大学子读研三年都没能有对象。南门卖水果的大姐,每次路过她的摊都会去买几个丑柑,在成都接触最多的人,可惜我硬着头皮也听不懂四川话。最搞的是门房间大爷,夜里去吃了个夜宵,回去晚了小区大门锁上了,大爷帮着开完门也不看证件就说要钱,问他要多少,一块两块随便给,真正哭笑不得。

成都现在有地铁了,相比西安的干净有序很多。去成都的前两天在西安意外摊上了沙尘暴,更是突显了成都的清爽。成都的火车也是一样,又方便又干净,从成都去都江堰,十五块钱三十分钟就好。都江堰是个有山有水有古镇的好地方,两千年前防洪灌溉,使成都平原成为水旱从人不知饥馑的天府之国,两千年后还能成就旅游观光事业,治水者可谓功德无量。如此说来,九十块钱拜拜水,还是拜的那么有技术含量的水利工程,不能算贵。

No.142012.04.13周五17:1318:00-09:00

江湖武汉

No.152012.04.17周二15:159:00-18:00

Selenium查找页面元素的方法

SeleniumAutomation

上个月整理自动化代码,最主要的工作就是把分散在各个case的界面元素整合起来,前几天和其他部分的同事聊起,正好也是在做这个工作,干脆在博客上分享一下。

这个事情是从开始写Selenium的自动化就想做的,当时底气不足没有一定要这么做,只是尝试写了个抓页面的工具未果然后不了了之,直接导致了后面维护成本非常高,MVC全都在一块,界面一改就一堆case fail。想到写工具来得到页面元素是因为实习的公司曾有过一个叫cocoon的工具来做抓取页面的工作,但是我并不知道工作原理,当时想的方法是得到页面然后解析XML,但是我们页面并不是用的XHTML,所以这个工作就碰到了很大的困难,想尽办法把HTML变成XML,几个一折腾就没再搞下去了。

这次换了更为轻巧的方式,直接用Javascript取Dom元素节点,在浏览器控制台直接运行就能取到了。我们要做的是自动生成一个页面所需要的元素节点列表,并且是key/value形式的:key相当于是变量名,所以想到直接用id或者class之类能够定位的属性值,如果是中文则转为拼音;而value则是选择器表达式(我现在用的是css选择器),如果有值相同的(比如没有id而class是一样的)就找其父节点直到可以区分为止。

首先在网上搜了一把找到一段实现中文转拼音的Javascript

后面的都好做,想取什么元素就取什么元素。


function getParentTag(node) {
    //最多取三级父元素
    var depth = 3;
    var name = node.tagName.toLowerCase();
    for(var i=0; i<depth; i++) {
        parent =node.parentNode;
        if(parent.getAttribute("id")) {
            name = parent.tagName.toLowerCase() + "#" + parent.getAttribute("id") + " " + name;
            return name;
        }
        else if(parent.getAttribute("name")) {
            name = parent.tagName.toLowerCase() + "[name='" + parent.getAttribute("name") + "'] " + name;
            return name;
        }
        else {
            name = parent.tagName + " " + name;
        }
    }
    return false;
}

//归一化,中文转拼音,小写转大写,空格转下划线
function normalization(str) {
str = str.replace(/\(|\)|:|:|>|</g, "");
str = str.replace(/\.|\s|-|#/g, "_");
str = str.replace(/_{2,}/g, "_");
str = str.replace(/^(\d)/g, "_$1");
str = str.replace(/^_+/g, "");
str = Pinyin(str);
str = str.toUpperCase();
return str;
}

//取当前页面名称
function getPageName() {
var paths = window.location.pathname.split("/");
var page = paths[paths.length-1];
var pagename = page?page:"index";
if(page.indexOf(".")!=-1){
pagenames = page.split(".");
pagename = pagenames[0];
}
return pagename;
}

//拼变量名
function setVariable(type, name, attribute, content) {
var key = normalization(name + "_" + type);
var value = attribute + "=" + content;
var count = 0;
if(variables[key]){
count = ++variables[key]["count"];
key = key + "_" + count;
}
variables[key] = {"value":value,"count":count};
}

function showVariables() {
for(var key in variables){
console.log(key + " = \"" + variables[key]["value"] + "\";");
}
}

//取所有链接
function links(){
var links = document.getElementsByTagName("a");
for(i=0; i<links.length; i++){
//注意: firefox没有innerText
if(links[i].innerText) {
setVariable("link", links[i].innerText, "link", links[i].innerText);
}
}
};

//取所有input
function inputs(){
var inputs = document.getElementsByTagName("input");
for(i=0; i<inputs.length; i++){
if(inputs[i].getAttribute("type") && inputs[i].getAttribute("type")!="hidden"){
if(inputs[i].getAttribute("id")) {
setVariable(inputs[i].getAttribute("type"), inputs[i].getAttribute("id"), "css", "input#" + inputs[i].getAttribute("id"));
}
else if(inputs[i].getAttribute("name")) {
setVariable(inputs[i].getAttribute("type"), inputs[i].getAttribute("name"), "css", "input[name='" + inputs[i].getAttribute("name") + "']");
}
else if(inputs[i].getAttribute("value")) {
setVariable(inputs[i].getAttribute("type"), inputs[i].getAttribute("value"), "css", "input[value='" + inputs[i].getAttribute("value") + "']");
}
}
}
}

//取所有button
function buttons(){
var buttons = document.getElementsByTagName("button");
for(i=0; i<buttons.length; i++){
if(buttons[i].getAttribute("id")) {
setVariable("button", buttons[i].innerText, "css", "button#" + buttons[i].getAttribute("id"));
}
else if(tagWithParent = getParentTag(buttons[i])) {
setVariable("button", buttons[i].innerText, "css", tagWithParent);
}
else {
setVariable("button", buttons[i].innerText, "css", "button" + ":nth-child(" + i + ")");
}
}
}

//取所有select
function selects(){
var selects = document.getElementsByTagName("select");
var currentSelect = "";
for(i=0; i<selects.length; i++){
if(selects[i].getAttribute("id")) {
currentSelect = "select#" + selects[i].getAttribute("id");
setVariable("select", selects[i].getAttribute("id"), "css", currentSelect);
}
else if(selects[i].getAttribute("name")) {
currentSelect = "select[name='" + selects[i].getAttribute("name") + "']";
setVariable("select", selects[i].getAttribute("name"), "css", currentSelect);
}
else {
currentSelect = "select" + ":nth-child(" + i + ")";
setVariable("select", selects[i].innerText, "css", currentSelect);
}
var options = selects[i].getElementsByTagName("option");
for(j=0; j<options.length; j++){
setVariable("option", currentSelect + "_" + options[j].innerText, "label", options[j].innerText);
}
}
}

//全局变量
var variables = {};
links();
inputs();
buttons();
selects();//其实这些代码都很类似只有很少量的差别,看看是不是可以精简
showVariables();

写完试了一下效果,发觉有时候页头页尾公共部分是不需要测试或者不需要重复测试的,所以可以把不需要的部分去掉,避免获取那部分的元素。


//如果有一块区域不取,可以先remove掉
var excludeNode = document.getElementById("list-content");
excludeNode.parentNode.removeChild(excludeNode); 

上面代码没怎么测试过,最近一直忙别的项目都还没来得及用。

其他部门的同学是想做一个浏览器扩展,从而实现鼠标点点就自动得到页面元素,我建议在Firebug的扩展FirePath上做修改,这个扩展本身就很好用,强烈推荐。当然如果能把Selenium IDE给改了就更完美了。

目前fix了之前的一些错误,文章里放代码不方便维护,把最新的js文件放出下载 ~ 复制代码后在浏览器的console中执行即可。

No.162012.05.19周六19:479:00-18:00

初学doxygen

doxygen

doxygen是一个开源的文档系统,类似于javadoc,根据代码中的注释生成对应的文档,支持java/c/c++/objective-c/python等多种编程语言。

“众所周知”,由于开发流程过于“敏捷”,我们的测试文档很稀缺,每位新同学想了解现有的功能都需要“言传身教”,没有文档可依,给大家带来很大的痛苦T_T,所以想到把doxygen用在UI自动化测试的代码中,维护代码的同时维护测试用例的文档,只是“举手之劳”,造福“后来人”。

///试用doxygen:

测试机上已经装有doxygen,(windows下也有的,跨平台,点这里下载http://www.stack.nl/~dimitri/doxygen/download.html#latestsrc,看文档更方便些)

在代码目录下,

1.    doxygen –g,在目录下生成一个Doxyfile的配置文件,
2.    doxygen  在html目录下生成文档

效果如图(截了一部分)

我在用的时候将默认的配置文件做了以下修改,如果有问题可以参考一下

DOXYFILE_ENCODING      = GBK      //生成文档的编码
JAVADOC_AUTOBRIEF      = YES       //使用javadoc的简短描述:取注释第一行为简短描述(比如上图“淘代码”即为简短描述,Detailed Description为详细描述,后面会说到)
INPUT_ENCODING         = GBK      //代码所用的编码
RECURSIVE              = YES          //如果项目只有一个源代码根目录,其中有多个子目录,那么只需指定根目录并把RECURSIVE标记设置为YES
GENERATE_LATEX         = NO         //默认会生成HTML和LATEX两种格式的文档,另外还可以有chm rtf等等格式的,暂时没有用到Latex

如果这段没说清楚,可以直接看下面那段的例子

刚才说到javadoc的简短描述,
每个代码元素有两种描述:简短的和详细的。简短描述通常是单行的。函数和类方法还有第三种描述体内描述(in-body description),这种描述把在函数体中找到的所有注释块集中在一起。

目前我想到的,能用到的主要就是类的简短/详细描述,函数和类方法的简短/详细描述,函数体中的注释块这些

详细描述:/** 这里写一点东西 */(多行注释)  也可以用/*!xxx */(多行注释),///xxx(单行注释), //!xxx(单行注释)来表示

简短描述可以用空行或者.(javadoc中认为.是第一句句子的句号)来截断:


/** 这是简短描述
*
* 这是详细描述
* 继续详细描述 \n
* 加个<a href="file://n/">\\n</a>输出到文档时才会换行
*/

或者


///这是简短描述

///这是详细描述

更多的可以参考http://www.cnblogs.com/wishma/archive/2008/07/24/1250339.html

http://www.stack.nl/~dimitri/doxygen/manual.html

另外java编码规范可以参考 http://huihoo.org/code/java_code_conventions.html#b1

举个例子


/** 测试url支持、在页面上没有筛选入口的筛选参数 这个是类的简短描述
*
* 加个换行让它多一个详细描述
*/
public class TestAuctionTag extends Search {
    /**
    * etao精选店铺筛选 ,这个是类方法的简短描述
    *
    * 这里可以写一些详细描述
    * @todo etao精选店铺筛选,没写完
    */

@Ignore("还没写完的case,不运行")
public void test_Filter_Etaoshop(){
///支持url参数筛选,auction_tag=2305
open("/search?q=a&auction_tag=2305");
///翻页/视图切换参数继承,不会丢失精选店铺的筛选项
//上线结束后接着这里继续写
///搜索结果页没有精选店铺icon
}
}

@todo会在related page里面生成一个todolist页,除了@todo外还可以有@bug @test @deprecated

有点大材小用的感觉,鉴于使用还是很简单的 && 懒得再重新写/维护一份测试用例的文档了,其实也就是写代码的时候多加点注释的事情~

No.172012.05.20周日15:039:00-18:00

Vim的几个好用插件

Vim

说来惭愧,用了三年的Vim一直都没用好,趁着别的同学开始火热学习Emacs之际,我决定,还是好好学习一下Vim好了。

首先确保Vim的版本在7.0及以上,因为后面提到的Snipmate需要7.0及以上Vim版本。

一、ctags

产生标记文件以帮助在源文件中定位对象。这可是看代码、写代码的利器啊。
  1. http://ctags.sourceforge.net/下载ctags源码
  2. 解压并安装
  3. 在代码根目录下敲入ctags -R,会生成tags文件
    1. 然后,
    2. 在运行vim的时候加上"-t"参数,例如: vim -t foo_bar 这个命令将打开定义"foo_bar"(变量或函数或其它)的文件,并把光标定位到这一行。
    3. 在vim编辑器内用":ta"命令,例如: :ta foo_bar
    4. 最方便的方法是把光标移到变量名或函数名上,然后按下"Ctrl-]"。用"Ctrl-o"退回原来的地方。 注意:运行vim的时候,必须在"tags"文件所在的目录下运行。否则,运行vim的时候还要用":set tags="命令设定"tags"文件的路径,这样vim才能找到"tags"文件。也可以在.vimrc中增加一行:
      
      set tags=tags;/
      
      这是告诉vim在当前目录找不到tags文件时请到上层目录查找。
  4. 新编辑的文件不会立即生效,因为所有这些都依赖于tags文件。可以将更新tags数据的命令绑定到一个快捷键上,在.vimrc中定义快捷键,这样直接在Vim中输入tg就可以更新tags文件了,不过一定要在代码的根目录下打开,不然就在子目录下生成tags文件了。
    
    map tg :!ctags -R
    
二、taglist 能够列出源文件中的tag并跳转,所以必须要先安装ctags。
  1. http://vim.sourceforge.net/scripts/script.php?script_id=273 下载tlist
  2. 解压
  3. 将解压后的目录和文件复制到~/.vim目录下
  4. 在Vim中输入:Tlist就可以用啦,可以看到本文件的function, class, variable等。点击名称还可以跳转到代码该名称的声明处。使用’+'和’-'可以对不同类型的变量进行折叠
三、snipMate 代码自动补全工具。
  1. http://www.vim.org/scripts/script.php?script_id=2540 下载snipMate
  2. 解压
  3. 将解压后的目录和文件复制到~/.vim目录下
  4. 用tab键就可以实现代码补全,使用方法可以看这个视频
  5. 如果需要自定义,可以编辑~/.vim/snippets/*.snippets,照样改写即可,比如java.snippets
    
    snippet t
        /**
         * ${1}
         */
        @Test
        public void test${2}() {
            ${3}
        }
四、TabBar 可以使Vim像浏览器那样有多个Tab,同时查看、编辑多个文件时会比较有用,结合ctags就更好用啦~。
  1. http://www.vim.org/scripts/script.php?script_id=1338 下载TabBar
  2. 解压
  3. 将解压后的目录和文件复制到~/.vim目录下
  4. 打开两个文件,用Meta+1、Meta+2就能在两个tab中来回切换。我的键盘上没有Meta键,可以在SecureCRT中设置session options -> terminal -> emulation -> emacs -> 选中use alt as meta key,这样就能用Alt+1、Alt+2切换了。不过SecureCRT中可以用Alt键+数字键切换SecureCRT的tab,这样就会和原来的使用有冲突,我们可以修改TabBar的快捷键,找到~/.vim/plugins/tabbar.vim修改"NORMAL mode bindings for vim( terminal)"之后的键盘映射关系即可,不过我没找到怎么修改为Ctrl或者Shift,只是简单地改成了用数字键切换:)用:bd可以关闭当前标签页。装了之后vimdiff的时候左边会多个tab导致显示会不齐,不知道怎么解决。
No.182012.05.27周日01:509:00-18:00

WEB安全 之 XSS基础

XSS

XSS是Cross Site Scripting的缩写,简单说就是攻击者让访问被攻击网站的用户运行不属于这个网站的客户端代码(一般是javascript代码)。

先举例说明。这里点击查看XSS攻击示例,试了下IE9和Chrome19会因为URL中存在脚本而阻止页面的脚本运行,如果有Firefox或者低版本IE(低版本Chrome没试过,高版本IE可以通过修改设置不被阻止)可以看到,TechWeb的Logo变成Baidu了。

刚才点过去的URL是:http://www.techweb.com.cn/search/index.php?type=%22%3E%3Cscript%3Edocument.getElementsByClassName%28%22logo%22%29[0].childNodes[0].childNodes[0].setAttribute%28%22src%22,%22http%3A%2F%2Fwww.baidu.com%2Fimg%2Fbaidu_sylogo1.gif%22%29%3C%2Fscript%3E%3Cinput%20type=%22hidden%22%20&q=s type参数中带了一段Javascript代码,查看页面源代码可以看到这一段<script>document.getElementsByClassName("logo")[0].childNodes[0].childNodes[0].setAttribute("src","http://www.baidu.com/img/baidu_sylogo1.gif")</script><input type="hidden" " />,可见Javascript代码已经成功注入并且由浏览器执行了。

由于Javascript运行是需要条件的,而运行Javascript代码可以是在CSS中也可以是在Javascript代码段中。
  1. css中,也就是在style:expression中是可以运行Javascript代码的。这种情况仅限于IE浏览器。
     <style type="text/css"> a {xss : expression(onmouseover=function(){this.style.backgroundColor="#F5F5F5"})} </style> 

    此处#F5F5F5的背景颜色是用户自定义的,那么如果将颜色改为#F5F5F5";alert(1);a=",如果没有做处理,得到的就会是

    a {xss : expression(onmouseover=function(){this.style.backgroundColor="#F5F5F5";alert(1);a=""})}

    mouseover的时候就能触发alert了。

  2. Javascript中,
    1. HTML属性:<a src="javascript:alert(1)">
    2. 标签间:<script>alert(1)</script>
    3. script标签中的语句段 <script>var arg="</script><script>alert(1)</script>";</script>
XSS的类型分为三种:反射型、存储型、DOM型。
  1. 反射型就像刚才举的那个例子,HTTP请求中带了攻击脚本,立马就能在HTTP响应中(所以是后台处理的漏洞)有体现的,就是反射型。
  2. 存储型就是攻击脚本被存储到了数据库或者文件中,服务器端(可能是别的应用或者别的页面)在读取了存储的内容后回显了,就是存储型。这种情况用户可能直接打开正常的页面就会看到被注入了。
  3. DOM型是由于页面中原有的Javascript代码操作DOM,DOM被攻击者修改了(所以是原来Javascript代码的漏洞),而导致的XSS漏洞。
针对这三种类型说明一下测试方法:
  1. 反射型 对整个输入(特别强调的是,整个HTTP请求都是输入,数据库取来的数据其实也是输入。HTTP请求包括GET、POST参数,COOKIE,URL,头部的REFERER等等)中每个地方都可以如下操作,
    1. 自己构造一个唯一的串,例如:myxsstestxxxx
    2. 将某个输入项(比如某个参数)替换为上面的串
    3. 查看HTTP相应中是否有这个串,并记录下来
    4. 根据HTML上下文决定,用哪种类型串来尝试,尝试攻击串,参考XSS cheat sheet,然后重新提交请求
    5. 如果能够找到相应的攻击串说明漏洞是存在的 这里的关键在于第四步,因为服务器端可能会做一些限制,比如encode或者长度限制,测试的时候需要想办法看看是否能绕过限制。 这种类型的XSS漏洞,用白盒的方法也比较容易发现。我司有一款牛逼工具可以通过追踪输出变量,看在赋值过程中是否有被编码来判断是否存在注入,少有误报(但有漏报:D),PHP有款开源的扩展做的也是类似的事情,点击看 PHP Taint – 一个用来检测XSS/SQL/Shell注入漏洞的扩展。不过只检测$\_GET/$\_POST/$\_COOKIE,同事后来改了下源码支持$\_SERVER变量。
  2. 存储型 基本思路和反射型一致,只是取数据不是从HTTP请求中取(所以反射型中说的白盒检测工具就派不上用场了),回显的地方可能并不在当前页面。
  3. DOM型 DOM型和反射型最大的区别在于,反射型在查看页面源代码时可以直接看到被注入部分的代码,而DOM型不能。 举例说明:被攻击页面testpage.html是这样一段Javascript代码:
     <script>document.write(window.location.hash);</script> 
    那么我们可以在url中这样注入:
    http://hostname/testpage.html#<script>alert(1)</script>
    此时查看页面源码还是原来的<script>document.write(window.location.hash)</script>,并没有出现我们注入的<script>alert(1)</script>。 这种类型的XSS用黑盒测试就没有像反射型的那么容易了,不过我们也可以通过灰盒测试的方法找到相关操作DOM的代码,依次判断是否存在注入。
     
    #js代码中URL相关方法 
    grep -R -P '(?<!encodeURIComponent\()document\.(location|URL|URLUnencoded|referer)' * 
    #js代码中输入参数相关方法
    grep -R -P 'document\.(write|writeln|body\.innerHTML|body\.innerTEXT)|window\.(execScript|setInterval|setTimeout)|\beval\b' * 
    

有则改之,无则加勉。防范XSS最基本的原则是“Filter input, Escape Output”,具体来说:
Filter input:不信任用户的任何输入,过滤;
Escape Output:不直接将原始数据输出到页面,转义/编码;
更多参考:http://security.ctocio.com.cn/wpsummary/26/8710026.shtml。 后面的blog中会对常见的XSS漏洞进行分析,看看如何防范。

No.192012.06.02周六17:2018:00-09:00

iPhone:随处的世界

iPhoneiphonegraphy

拍照是一种美德。要能无论何时何地都能拍下眼中的心底的世界,那更是美德中的美德了。这次要做的就是重新熟悉手上的iPhone,要知道iPhone随手拍出来的照片也可以是很惊人的。

西安大雁塔前的喷泉广场,那次旅行忘了带SD卡,所以相机没派上用场,倒是意外发现iPhone的拍摄效果也非等闲之辈

iPhone 4
传感器类型 背照式CMOS
传感器尺寸 1/3.2英寸
有效像素 500万像素(4S为800万像素)
镜头焦段 约35mm
固定光圈 f/2.8(4S为f/2.4)
微距性能 最近对焦距离约6cm
视频拍摄性能 1280X720,30fps

列出这些不是为了将iPhone和DC或者其他手机做对比什么的,我不是果粉也没有黑卡片机的意思。要拍好照片,了解自己的相机还是有必要的。(可惜iPhone我还是借来的T_T)

一、对焦
AE/AF锁定功能,也就是曝光锁定和对焦锁定。在拍摄界面中长按画面某一位置,对焦测光方框会闪动两下,表示已经启动AE/AF锁定,这时无论取景画面怎样移动,测光和对焦都不会改变,直到手指再次点击屏幕取景位置,AE/AF锁定就会解除。
iPhone自带的相机目前是只能同时锁定,如果只需要锁定曝光或者对焦其中之一,或者分别进行锁定,可以用camera+。

二、曝光
主要就是上面说的AE锁定功能了,这种方法对拍逆光照片很有用,不论是想提亮主体还是拍剪影效果。pudding camera还有曝光补偿的功能,不过有AE锁定,曝光补偿感觉也可有可无了。

城墙上捉风的少年
三、构图
iPhone提供了辅助构图线,可用于经典三分法(九宫格或者井字格)构图,安排画面的主体在辅助线的4个交点上,表现鲜明,构图简练 —— 而且对初学者而言易学易用。构图这东西不好说怎样才是更好的,不过既然这个理论由来已久那也是经得起考验的吧,毕竟也是接近黄金分割的。

临安青山湖
尼康卡片机拍的,在手机了翻了好久也没找到用这种构图的照片,临时处理了一张,只作说明之用

另外还可以在拍风景、建筑时用来辅助看清水平或者垂直线。

江湖武汉
四、夜景
拍夜景主要是要注意防抖,需要固定好手机,然后轻轻的去按快门,有时候还不一定够轻,以致按快门的动作震动了画面,造成模糊。有两个方法防止抖动:
一是用定时,比如倒计时10秒拍摄,一般是用于自拍的,那么这个时候就不需要手动去触屏或者按音量键了。印象中camera+就可以。
另外一种就是用遥控快门线——iPhone的声控耳机可以用来做这个事情,按下音量键就可以拍照了。这个功能很酷很有用~

五、微距
单反相机的景深大小控制取决于三个因素:相机镜头光圈的大小调节、所用镜头焦距的长短,以及拍摄距离的远近。然而,iPhone可控制的只有最后一个因素,即拍摄距离的远近。越接近拍摄对象,景深就会越明显。淘宝上有卖长焦、广角、鱼眼、微距镜头,广角+微距的30多块就能拿下,没买过~
有人说在镜头上滴一滴水也可以拍出很好的微距效果,免费还方便,有待考证:)。

六、街拍
这招是看阿默先生的采访时学来的:偷拍的时候可以将手机上下倒过来拿,这样摄像头就置于下方,右手只要点下屏幕上的拍照就好了。单手操作,毫不别扭,乍眼一看真不知道是在拍照。

七、延时摄影
就是用慢速快门可以拍一些有趣的效果,比如夜间车水马龙,水面暗波流动,天空风起云涌,还有光影绘图或者拍烟花闪电什么的,slow shutter可以用。斗转星移的效果估计是指望不了了。

八、多重曝光
据说FusionCam可以拍,不过应该也就是后期的叠加效果吧,没什么技巧。前阵子看到19楼有一辑照片用的多重曝光,还挺不错。《距》-此片献给所有相信爱情的人

九、HDR
iPhone开启HDR后会连拍三张照片,分别对应欠曝、正常曝光和过曝,再合成为一幅照片,这样的照片暗部和亮部的曝光都是正常的,可以提升细节表现。

十、全景照片
微软的photosynth可以做到,目前iOS好像还没有开放自带的全景拍摄功能。

相关链接: iPhone摄影:轻触的浪漫 推荐软件合集

由于手上没有iPhone,有些照片得以后再补啦。

No.202012.08.07周二06:3118:00-09:00

用Vim发新浪微博

Vim

前几天把笔记本上的gVim给小搞了下,正好看到这篇用Vim(gVim)发腾讯微博-weibo.vim,就想试试把腾讯微博改成新浪微博的,可以学习下OAUTH,顺便还能看看Vim的插件开发。

Vim插件开发看这里:Writing a Vim plugin
OAUTH怎么用看这里:Google OAuth 2.0 Playground

新浪微博授权机制说明

第一步,用户对应用授权,获取Code

https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI

其中YOUR_CLIENT_ID和YOUR_REGISTERED_REDIRECT_URI可以在应用信息中找到,YOUR_CLIENT_ID是新增应用时给的,YOUR_REGISTERED_REDIRECT_URI是选择应用后应用信息->高级信息中填写的回调页。
如果用户同意授权,则会跳转到回调的页面,url后面会跟code参数。

第二步,换取Access Token,Access Token用于访问用户的资源

https://api.weibo.com/oauth2/access_token
POST参数为 client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=authorization_code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI&code=CODE

第三步,使用获得的Access Token调用API

新浪微博的API文档

还是有两个地方很困惑。同样是OAUTH2.0,新浪的Code换取Access Token只能换取一次,再请求的时候就会出现400错误,所以客户端要用的话必须把Access Token保存下来,否则就需要重新获取Code来换Access Token(因为这个问题所以现在每次启动Vim都需要先更新Code不然就提示Access Token错误)。但是腾讯的是可以多次获取的,Access Token要用的时候再请求一次就好了。另一个不同点是新浪和百度需要将YOUR_CLIENT_SECRET和CODE一起传给授权服务器才能换取Access Token,腾讯却不需要YOUR_CLIENT_SECRET,不解,不用这个怎么验证应用的合法性呢。对OAUTH这个认证机制不理解。当然腾讯的认证也没有实际操作过,只是很简单得看了下那个Vim插件的代码。

下载破旧简陋的Vim插件

No.212012.08.07周二06:3918:00-09:00

桌上足球高端玩家成长之路

Foosball

去年11月开始玩Foosball,因为同事不论会打不会打的都在玩,就也凑个热闹。后来组了队一起玩,队友擅长打前锋,我自然就成了后卫。前阵子和队友谈起这段时间的成长,感觉还蛮好玩的。

新人最常见的问题就是使不上力气,球杆容易脱手,摸不到球各种漏,这些都是初学者的特征。可以检查一下站姿和握杆的手法是不是对,因为如果没有问题,那就只差多多练习找感觉。我当后卫的经验是,一般站姿是左脚在前右脚在后,因为后卫的活动范围比守门员要大,右手臂需要更大的空间自由伸展,并且这个角度视野相对开阔。

下面是我和队友对前锋和后卫不同层次玩家的定义,其实也就是我们自身的写照。

前锋篇:
入门:乖乖把脚抬起来给后卫的进攻让道。入门没学好就容易堕入飞轮邪道,噪声不绝于耳,徒劳却无功;
进阶:会反弹对手打出来的球;学习打斜线,抓住稍纵即逝的机会;
高手:能够把握机会持续进攻,各个方向的斜线都能打得流畅,能控球(后卫可以停球但对前锋很难),中场和前锋能打些配合;

后卫篇:
入门:用守门员加后卫的两只脚由点成面形成防守;
进阶:及时把球打出去让前锋接手,不被对手的前锋反弹;学习打斜线,在某些位置能进球;
高手:不着急把球打出去,调整好角度再打;各种角度都可能进球;能给队友传好球;能判断球的路线做好防守;

至于想要成为顶级的玩家,都还长路漫漫。我们希望最终能做到静若处子,动若脱兔;平时不露锋芒,锋芒展露时,必是致命一击。

公司里玩的人很多,不过大家都是业余娱乐,与专业选手自然不能比,就好比人家是写操作系统的,我们却一直在写Shell脚本。小记一笔,止增笑耳。

No.222012.09.16周日18:299:00-18:00

自动检测WEB漏洞的浏览器扩展

SearchXSSXSSBrowserFirefoxChromeExtensionTool

两三年前就开始写的一个扩展,当时只是和赵固同学一起学习怎么写扩展,一度只有个架子,不知怎么检测。随着对安全漏洞的了解加深,思路也日益明朗起来。目前已做到自动检测反射型XSS漏洞,鉴于扩展可以完整得到请求和响应,理论上是支持多种WEB漏洞的检测的。

反射型XSS漏洞的检测方法一般是将某个输入替换为一个攻击串,然后在响应中检查是否存在响应的攻击串,参见前文《WEB安全之XSS基础》。简单来说就是这样的一个工具:在浏览器发出请求的同时,依次替换请求的各个输入为某个(或者某几个)攻击串,在每个响应中检查是否存在XSS漏洞。

此扩展的特点在于,

1.所有的检测步骤都是在浏览网页时自动进行的,无需人工干预,几乎不影响浏览器的日常使用,即使非技术人员也可安装使用。同时可以有漏洞日志自动收集,扩展发现的漏洞可以统一管理。

2.扫描性能也不是问题,由于日常使用时发出的请求(对于开发/测试而言,则是功能测试时的请求)基本可以覆盖所有的输入,检测时无需爬虫,比起APPSCAN,WVS的XSS扫描会快速准确很多。

3.实时检测,可以帮助开发工程师在自测调试时发现安全问题,对于测试工程师而言可以将功能测试与安全测试并行进行。将安全测试提前、实时进行无疑是减少了工作成本。

下载地址:http://sipaizhao.de/download/searchxss.xpi

ps.不知道firefox扩展如何能做到自动更新,试了update.rdf却始终没有奏效。╮(╯▽╰)╭

No.232012.09.16周日19:2318:00-09:00

53paper

paper

前阵子很迷ipad上的53 paper,超赞的应用,尤其是撤销、恢复的那个交互太令人难忘了,以至于现在在白板上画也会习惯性得手指转两圈。

放几张图,让诸位见笑了。

买了支触控笔adonit jot pro,效果还不错,用手指好多细节不好画,原本有点担心会画伤屏幕,看来有点多余。

网上还有篇paper的攻略,Make with Paper 53 绘画技巧分享,画得真好啊,羡慕~

No.242012.11.25周日13:349:00-18:00

WEB安全 之 CSRF基础

CSRF

所谓的CSRF漏洞,就是跨站请求伪造(Cross-site request forgery),攻击者让用户发送请求给服务器以达到修改数据的目的。

举例来说,之前测试的一个邀请系统存在这样一个CSRF漏洞。

比如这个是我的邀请链接,正常人是这样发给好友的。简称邀请链接。
http://sipaizhao.de/01234567890.htm

这个是接受邀请之后的链接,需要用户通过上一个链接点确认。简称接受链接。
http://sipaizhao.de/accept.htm?code=01234567890

但是,问题出在这里,用户不通过上一个邀请链接点确认,也可以直接请求这个接受链接,只要访问了,就接受了攻击者的邀请,这个邀请关系会被存在数据库中,攻击者会得到收益。

于是,攻击者出动了:在个人博客里暗暗得埋了个src是这个接受链接的img标签,只要用户访问其博客就会暗暗得发送这个请求,而用户根本不知情。

发送了请求之后,如果用户在该网站已经登录,就默默得被接受了邀请。由于涉及到修改数据的操作一般都是需要用户认证的,所以判断是否存在CSRF漏洞一个条件就是,这项操作需要用户登录。

解决CSRF漏洞的方法一般是在用户请求的页面中增加一个加密随机数,在生成随机数同时添加到当前请求和cookie中,再在服务器中判断请求中的随机数是否与用户cookie中的一致,比如请求变为http://sipaizhao.de/accept.htm?code=01234567890&t=abc123,因一般情况下用户的cookie是攻击者无法得到的(除非有xss漏洞等),所以攻击者无法知道应如何构造请求,也就达到了防范的作用。

一般会建议使用POST请求来代替GET请求,因为GET请求可能会使攻击者通过referer等方式得到用户的随机数,如果随机数在POST参数中,攻击者是无法得到的。无论收藏夹、ACCESSLOG还是REFERER中都只有URL,不会存在POST参数。

另外有一种方法是判断请求的referer,比如accept.htm的请求http://sipaizhao.de/accept.htm?code=01234567890必须是http://sipaizhao.de/01234567890.htm,但由于referer可以伪造,正常用户的浏览器也可能不发送referer到服务器端,这种方法并没有太可靠,但此方法可以用于检测是否有CSRF漏洞被利用。

查看邀请系统服务器的access log:
222.222.222.222 1262 - [17/Oct/2012:10:00:00 +0800] "GET /accept.htm?code=1234567890 HTTP/1.1" 302 0 "http://bbs.sipaizhao.de/123456.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"

refer的链接:
http://bbs.sipaizhao.de/123456.htm (此处真实链接隐去)

查看此链接,这个家伙发帖的时候加了隐藏的img,src为存在漏洞的URL
<p><img style="height: 1.0px;width: 1.0px;float: none;margin: 0.0px" src="http://sipaizhao.de/accept.htm?code=1234567890"/>信用就是金钱!</p>

2013
No.252013.01.23周三16:119:00-18:00

Selenium并行测试提高效率

SeleniumAutomation

Web自动化测试让人很头疼的一个问题就是Case执行速度太慢,之前柬之同学自己用多线程的方式分配到多个Selenium RC从而达到并行测试的目的,去年柬之同学做截图对比服务时发现了解决这个难题的终极方法Maven Surefire Plugin + Selenium Grid

Maven Surefire Plugin是一个Maven的插件,可以并行执行mvn test。只需要在pom.xml里面加上这个插件,在执行测试用例的时候就会自动并行测试了。


<plugins>
…
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.13</version>
<configuration>
<parallel>methods</parallel>
<threadCount>4</threadCount>
</configuration>
</plugin>
…
</plugins>

Selenium Grid的使用上面的文章里也写得很清楚啦,启动Grid时把Hub和Node运行起来,Case里只需要指定Hub的机器和端口就可以,Hub会自动分配到各个node上。

start-grid.bat:


set file=d:\webdriver\selenium-server-standalone-2.28.0.jar
set hubport=4000
set num=4
set server=127.0.0.1

start java -jar %file% -port 4000 -role hub

for /l %%i in (1 1 %num%) do (

set /a nodeport=%%i+%hubport%

call:start-node %nodeport%

)

:start-node

start java -jar %file% -role node -port %nodeport% -hub http://%server%:%hubport%/grid/register -nodePolling 300000 -registerCycle 299000

ping 127.0.0.1 -n 3

No.262013.02.20周三11:449:00-18:0018:00-09:00

Bootstrap版杀人游戏

Bootstrap

之前学Yii的时候想做一个杀人游戏的小网站,那时调css搞了很久还是被同事一眼看穿——这个什么项目啊,没有UED吧?前阵子看同事做后台界面用的Bootstrap,很简单很方便很美观,解决了我等对界面高要求又低水平程序猿的难题,绝对是完美主义与拿来主义者的福音,以后有什么想法可以立即实现岂不妙哉。

Bootstrap是Twitter推出的一个用于前端开发的开源工具包,官网地址是http://twitter.github.com/bootstrap,还有个中文的网站http://www.bootcss.com

应用在此,http://sipaizhao.de/mobs,完全Javascript实现,直接看页面源码就能看到本人拙劣的编码功力了。试了几个手机效果都还不错,睡觉吃饭居家旅行必备良物:)

再把书签保存到手机桌面就可以佯装成一个App啦。

用了一晚上用BootStrap做了个之前SearchXSS后台,写个小应用可方便了。

No.272013.05.02周四07:2118:00-09:00

海淘iRobot

iRobot海淘省钱

前阵子打算给Fine妈买个iRobot打扫房间讨好她顺便收拾未来小宠物掉的毛(关于小罗伯特的一切请点这里),国行价格太高犹豫了好久没下手,淘宝上最便宜的也得3000多。后来在什么值得买上面看到Amazon.com上iRobot Roomba 650打折,299刀实在是太划算啦,赶紧看了海淘攻略,在亚马逊下了单(还买了淘宝上的礼品卡又省了不少)。亚马逊用联邦快递把Roomba寄到百通的转运仓处。3月9号下的单,3月11号从SPARTANBUG(斯帕坦堡,南卡罗来纳州)发货,3月13号到East Brunswick(东布兰斯维克,新泽西州)。

到百通的转运仓之后,记得要去新增运单,我们仔细阅读了百通网线路介绍,选择了百通优先线A,运费是86刀。接下来就是漫长的等待时间,百通这个入关状态持续了将近一个月,期间找了客服给了等于没有的回复,就在想下次换一家转运公司时,4月26号突然就收到了iRobot,真是surprise啊。根据快递单号查了一下,EMS在4月24号收件,4月26号就送过来了,还是很快的。

这次海淘一共花费299+86=385刀,折合人民币2500不到,在海关还没有交税(^_^怎么会这样),太好啦。

不过这个坑姐的小罗伯特的电压问题真是让人好忧伤,现在送到淘宝一家店里修去了…… 希望赶紧醒来干活呐,前两次工作还是收集了好多灰的。

看看小罗伯特在地毯上的大作:)

P.S.关于省钱这回事
一淘优惠购(http://ok.etao.com)优惠券、返利、优惠码,多重优惠。首页有个优惠购小助手,相当好用。
阿里妈妈(http://www.alimama.com)自己赚自己的佣金,但只能在淘宝天猫买东西时候用。
优惠券网站有一淘优惠券(http://quan.etao.com)、券妈妈(http://quanmama.com)等。
返利网站也有很多不过我基本不怎么用,因为优惠购实在太方便了。

P.S.关于亚马逊的省钱这回事
在亚马逊上买东西一般是没有优惠券和返利的,这个时候可以在淘宝上买礼品卡,比如中国亚马逊的话,可以在淘宝搜索@hotmail,我经常在这家店买礼品卡,发货很快也没出过错(没给我广告费啊~)。
可以再装个Chrome的亚马逊价格趋势扩展,这样就可以知道自己有没有在最低价位的时候入手了。

No.282013.07.23周二02:119:00-18:00

JDB和Java远程调试

DebugJava

关于JDB和Java远程调试。

之前师兄有分享过java远程调试的方法:

1. server上java运行的时候加上这个选项
JAVA_OPTS="$JAVA_OPTS -Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=n"
2. 本地debug时指定ip和端口8787就行了,eclipse选择Remote Java Application

因为笔记本开eclipse太慢了,有时候只是临时看个问题,所以简单看了下怎么在Linux下用JDB调试:

jdb –help查看帮助
jdb -connect com.sun.jdi.SocketAttach:port=8787,hostname=127.0.0.1

设置断点:
stop at com.taobao.qa.ATest:69 在ATest这个类第69行打断点
stop可以查看所有断点

cont 效果同eclipse里按F8 Resume
step 效果同elcipse里按F5? step into
next 效果同eclipse里按F6 Step Over

查看
locals 输出当前堆栈帧中的所有
dump 输出所有对象信息
print 输出表达式的值

针对后退键会出现^H问题的解决方法:(也可以用ctrl + w删除一个单词。但不能解决左右键定位的问题,唔没深究)

参考资料:
http://hi.baidu.com/widebright/item/bcddbc2596ba2a0977272c5c

No.292013.08.03周六00:3918:00-09:00

上海至杭州两日游

Hangzhou

迎来送往不少上海游客啦,趁这次姐姐来玩,要把计划记录下来。

D1:杭州东站 - 拱宸桥/信义坊 - 湖滨路酒店 - 浙大 - 宝石山 - 酒店(这是吃货路线吗?下面都是推荐吃的啊)

1.上海火车坐到杭州东站,算好时间坐上濮家码头的水上巴士运河线(3块钱),前往拱宸桥/信义坊(没坐过濮家站的,不知道方案可行不 ~)

濮家码头发船时间7:00 7:30 8:00 8:50 9:20 10:10 10:40 11:30 12:00 12:50 13:20 14:10 14:40 15:30 16:20 17:10 17:40 18:10

拱宸桥是京杭大运河的终点,附近大兜路上有香积寺还有富义仓,可以感受杭州的历史文化。这里还有好几个美食街。附上大众点评链接
吃过的也不算多,简单介绍一下吧。
蓝莲餐厅在运河旁边,环境不错的,菜感觉不像杭帮菜,更像创意菜..和绿茶风格比较相近。在龙井也有家店一直有耳闻环境好,没去过。
外婆家虽然上海也有,胜利河美食街的外婆家运动会主题餐厅可不是哪里都有哦。
去过大兜路的江南阿二,价格有点贵,味道也不如旁边的绿茶。

2.公交/开车去酒店办理入住手续(地铁没有直达的)。如果有时间可以去南山路学士公园罗马广场南侧办理公园卡,下半年办只用半年的年费,如果之前没办过卡的话要工本费。记得带好身份证。

3.去浙大吃晚饭。浙大旁边青芝坞有不少好吃的,学校里的食堂也还不错,平时上班经常去留食吃,又便宜有好吃,顺便还能感受一下学生时代的氛围。附上大众点评链接
吃过两家,老地方吃烤鱼,还有热意餐厅吃创意菜,都还不错。

4.从浙大走到黄龙洞上宝石山(小心毛毛虫T_T 这里貌似一般没有蛇的),溜达一圈就到了北山路啦。

5. 夏天的杭州可是很闷热的,但是西湖旁边总是这么凉快。沿着北山路散步,凉风习习。晚风吻尽荷花叶,任我醉倒在池边。
另外可以走走断桥,今年装了人工雾。湖滨路旁边有音乐喷泉还会放白娘子的主题曲,应景吧。

PS.如果住宿的地方在西溪湿地附近的话,可以去免费的福堤慢生活区喝喝小酒看看萤火虫。

D2:自由活动

办好公园卡之后很多景区都免费了,比如植物园、动物园、虎跑、灵隐景区(灵隐寺还是要收费的),西溪湿地门票可以便宜到10块钱。
九溪是比较好走的山路,可以溜达到龙井、梅家坞等地方。龙井路和梅灵北路交叉口那边还有个美食街,弄堂里还不错哦。
如果想睡个懒觉的话可以在保俶路吃饭,推荐丁哥黑鱼馆和渝香隆火锅。
要是闲得慌也可以去我厂玩,万塘路美食街等着你。
回家如果在城站坐火车的话,推荐附近有家舟昊鸭舌(不知道还开着不),很好吃的。回去前可以在铁道大厦的外婆家吃饭,一点也不赶。如果想去吴山广场逛的话,建议吃饭再往里面走一点,大门口的店家大多都不靠谱。

最后,强烈推荐秋天来杭州,气温适宜,满城的桂花飘香,朝晖公园的满地银杏,梅灵路两侧色彩缤纷,西湖边的梧桐秋叶,西溪的芦花摇曳...想想都美得有点感动呢。
虽然喜欢夏天,独特的只是夏夜晚风,可哪里的夏夜晚风不一样呢。秋天,杭州却是独一无二的。

柳永《望海潮》
  东南形胜,三吴都会,钱塘自古繁华。烟柳画桥,风帘翠幕,参差十万人家。云树绕堤沙,怒涛卷霜雪,天堑无涯。市列珠玑,户盈罗绮,竞豪奢。
  重湖叠岳清嘉。有三秋桂子,十里荷花。羌管弄晴,菱歌泛夜,嬉嬉钓叟莲娃。千骑拥高牙,乘醉听箫鼓,吟赏烟霞。异日图将好景,归去凤池夸。

No.302013.08.03周六02:209:00-18:00

IDEA使用

IDEA
【玄冰首发】

配置篇

1.选择jdk版本

点击File>Project Structure

2.配置maven

3.删除项目

先关闭项目,然后界面上不会是有项目例表,鼠标移到你想要删除的项目上(不要点击,一点就打开了),然后按DELETE键

4.启动时不打开工程文件

Settings->General去掉Reopen last project on startup.

5.打开多个工程

6.让光标不随意定位

Settings->Editor中去掉Allow placement of caret after end of line。

7.IDEA显示中文 Setting -> Appearance
选中Override default fonts by (not recommended),选择一个中文字体(显示成方块的就是)
8. 快捷键

Ctrl + Shift + F

全文搜索文本

Ctrl+F

当前文档搜索

Ctrl + Shift + N

搜索文件

ctrl+b

跳转到方法声明处

ctrl+alt+b

跳转到方法实现处

Ctrl + E

最近打开的文件(默认10个)

Ctrl + N

打开一个类

Ctrl + H

查看一个类的父类、子类等

alt + F7

查看method在哪里被使用了

Ctrl + F12

查看method在哪里被使用了查看当前类有哪些函数。在一个源文件代码比较长的时候,非常有用。

 

F7

进入函数

F8

下一步(执行这一行)

F9

继续执行到下一个断点

alt + F9

执行到当前光标位置

alt + F10

查看当前运行到哪里了

 

alt+1

打开(关闭)project窗口

alt+2

打开(关闭)收藏夹窗口

alt+5

打开(关闭)debug窗口

alt+6

打开TODO窗口

alt+7

打开Structure窗口

Ctrl+F4

关闭当前编辑页面

 

ctrl+d

复制当前行

ctrl+y

删除当前行

ctrl+x

剪切当前行

Ctrl + W

按一个word来进行选择操作(依次扩大范围)

Ctrl + /

注释/反注释指定的语句

Ctrl + Shift + /

进行多行语句的注释

alt + 向左箭头

文件后退

ctrl + alt + 向左箭头

位置后退

Ctrl + Alt + O

优化import自动去除无用的import语句

Shift + F6

提供对方法、变量的重命名

Alt + Insert

增加getter和setter

No.312013.08.20周二16:049:00-18:00

不是bug,是程序没有运行好

Work

这个故事源于测试MM周五要写周报时想起来周一有一个没有来得及跟进的线上bug,于是质问开发DD。故事的高潮发生在某天深夜测试MM翻测试DD的WIKI,发觉测试DD原原本本得还原了事件始末,其间人物性格鲜明,对话妙趣横生。测试DD的文笔之出众,才华之洋溢,令测试MM惊为天人!

测试MM:周一那个bug怎么回事

开发DD:那不是bug

测试MM:是bug

开发DD:不是bug

测试DD:就是bug

开发GG:不是bug,是程序没有运行好……

测试MM:那你后来改代码了?

开发GG:没改代码

测试MM:你改了

开发GG:没改

测试DD:他改了的

开发GG:他没改,他只是优化了一下……

从这个故事中可以总结出两点:
1. 写周报的重要性
2. 测试MM很羡慕开发GG的好口才

Just for fun:)

No.322013.11.01周五16:279:00-18:00

来往,微信,易信产品对比

无线

时间关系没有很深入研究,希望大家都能越做越好,这样用户就有福啦~

No.332013.12.26周四17:599:00-18:00

用TinyHTTPProxy部署HTTP代理

HTTP

最近经常需要查看手机的网络请求,找到个用Python写的133行代码的小工具,自己扩展下可以满足记录、转发、Mock等工作。由于是Python脚本,可以跨平台运行,这也是其一大优势。

如何部署HTTP代理:
  1. 下载地址:http://www.oki-osk.jp/esc/python/proxy/TinyHTTPProxy-0.2.1.zip
  2. 解压后,python TinyHTTPProxy.py 可以看到请求内容。
测试是否正常使用,可以在本地curl:
  1. curl -x 127.0.0.1:8000 'http://s.taobao.com' 默认端口是8000,可以在启动时指定端口参数。
手机选择网络后设置代理,填入Proxy机器IP和端口即可。

一般的使用场景无非以下几种,代码稍作修改即可满足需求
1. 记录
请求:找到self.log_request(),在前面增加print self.headers可以输出请求头
响应:找到out.send(data),在前面增加print data(建议先判断content-type是否为Html/Json再输出,如果是压缩过的数据需要在所有数据整合之后再解压)

2. 转发重写
重写的好处在于很多情况下无法很方便得调整参数、请求头等信息。
举例说明,如果需要修改UA为anclient,那么在转发(soc.send)前增加self.headers['User-Agent'] = 'anclient'。

3. Mock响应
Mock响应对测试非常有用。举例说明,当发现调用了api=everyday的接口时,返回一个失败状态的Json,那么在do_GET一开始增加判断
if path.find('api=everyday') != -1 :
self.connection.send('{"status":false}')
self.connection.close()
return

No.342013.12.26周四18:089:00-18:00

在IDEA下部署Android环境

IDEAIDEAndroid

前段时间有个作业,要通过源码打个安卓apk包,用IDEA打成了,所以做个记录~以下用是的IDEA12.0.2版本。

  1. 在IDEA设置中打开Android Support。在File->Settings中打开设置,勾选Android Support选项如下。
  2. 下载ANDROID SDK,从SDK Manager安装。这步和IDE无关。http://developer.android.com/sdk/index.html
  3. 导入工程时需要选择Android SDK
  4. 运行时选择module,编译、打包、模拟器打开成功!得到一个apk文件~
  5. Tools->Android->DDMS,IDE无关,只是启动DDMS。 http://blog.jetbrains.com/idea/2012/03/launching-android-tools-right-from-intellij-idea/

期间遇到的问题:

  1. import工程的时候提示:Error when importing module 'etao_android': Cannot find appropriate Android platform for API level 16 解决方案:下载安装Android SDK 4.1.2(API 16就是对应的4.1.2版本)。
  2. 打开SDK Manager下载的时候会遇到看不到4.1.2版本,无从下载。 解决方案:绑定host(万恶!)74.125.237.1        dl-ssl.google.com http://blog.csdn.net/core__code/article/details/11522041
  3. 编译时提示错误:android-apt-compiler: Cannot run program "D:\android-sdk\platform-tools\aapt 解决方案:将build-tools/17.0.0/aapt.exe, lib目录分别拷贝到platform-tools目录 http://www.cnblogs.com/bluestorm/archive/2013/05/31/3110617.html
2014
No.352014.02.18周二03:329:00-18:00

用好junit——如何利用自动化case做适配/兼容性测试

Automation无线客户端

最近小朋友们写了不少手机客户端的自动化用例,于是想着可以利用这些自动化case提高适配测试的效率,只要获取所有的设备ID列表循环执行每个case即可。同理,如果需要测多浏览器、多个abtest都可以用这个方法。

junit4有一个特性叫参数化测试,数据准备需要放在一个方法中,测试时每个用例会将其返回的Collection遍历一遍。

五步创建参数化测试用例:
  • Annotate test class with @RunWith(Parameterized.class)
  • Create a public static method annotated with @Parameters that returns a Collection of Objects (as Array) as test data set.
  • Create a public constructor that takes in what is equivalent to one "row" of test data.
  • Create an instance variable for each "column" of test data.
  • Create your tests case(s) using the instance variables as the source of the test data.
我们的测试用例改造:

@RunWith(Parameterized.class)
public class CatSearchTest{
    protected static IAndroidDriver driver;
    protected String deviceId = "";

public CatSearchTest(String deviceId) {
this.deviceId = deviceId;
}

@Parameterized.Parameters
public static Collection getDeviceId() {
return Arrays.asList(new String[]{"hello"}, new String[]{"world"}); //这个方法中需要实现用apktools获取deviceId列表
}

@Test
public void 首页入口() {
System.out.println(this.deviceId);
driver = AndroidRemoteDriver.start("com.taobao.taobao.sword.test", this.deviceId); //每次运行使用不同的deviceId
}
}

参考资料:http://www.tutorialspoint.com/junit/junit_parameterized_test.htm

apktool下载地址:https://code.google.com/p/android-apktool/downloads/list

No.362014.03.20周四13:049:00-18:00

客户端劫持事件真相

一笔超过万元的巨款,每天百万的流量,困扰数十万手机客户端用户的难题,一系列错综复杂的线索,各色人物粉墨登场。
测试团队精心调查,却在蛛丝马迹背后,发现了网络劫持的惊人真相。

这个故事发生在今年1月份,客户端发布了新版本,这一天测试同学和往常一样翻阅用户的反馈,但是和往常不一样的是,这天有十个安卓客户端的用户反馈服务不可用。使用和用户同款手机同样网络却一直无法重现,无奈之下,一方面将问题反馈给开发同学,一方面联系自己的亲朋好友,希望他们可以帮助我们重现。

事情已经发生了好几天了,就在我们束手无策的时候,一个在当地的朋友联系我们,她可以在她的办公室里重现,在家里就能正常使用。于是这天下午和开发同学一起实地走访,但是灵异的事件发生了,朋友和她的同事,同样的电信定制手机,同样的网络,同事的手机就能正常使用。就在我们已经打算把手机带回公司排查的时候意识到,离开这个网络环境也许就无法重现了,于是把手机上的日志传给另外一个同事帮助我们排查,才知道原来是一个已知的Bug,网络切换的时候才会出现,在最新版本中已经修复。帮朋友手机上的客户端升级之后问题消失,但这是唯一一个当地用户反馈服务不可用的,老版本用户的问题解决了,新版本用户到底遇到什么样的状况呢。线索中断了。

回到公司之后发现很多老板也关注到了这个问题,并且给出建议,可以从已经出现了20天的服务器超时问题开始排查。这天是周五,当晚也确实排查到一个服务器的Bug,差点以为大功告成。

周六早上继续舆情监控,发现问题其实并没有解决。于是推倒重来。用户反馈的特征有:
1. 只有安卓客户端有反馈(因为iOS客户端做了反劫持)
2. 新老版本安卓客户端都有反馈,排除掉某些用户可能是之前所说的网络切换Bug导致服务不可用,所以主要关注新版本客户端
3. 用户反馈集中在某几个城市的某运营商网络
4. 从时间上来看就出现在我们第一次发现问题的时候,并且之后反馈量稳定在十几二十个

但是最关键的是,用户反馈的很多是服务不可用,但还是有些用户提供给我们非常明确的信息,从表现来看其实分为两类:服务慢,以及网络连接失败,重新请求服务。

于是PE和服务端开发继续从服务慢的角度开始排查服务器超时问题,我们则是另外想办法分析网络连接失败的问题。

我们一方面可以通过监控看到每个城市的网络连接情况,从网络慢的城市上来看倒有些许符合用户反馈特征,但是从运营商角度上却并没有特别的,所以和特征不相符,排除网络问题的嫌疑。

另外一方面分析了客户端的日志,发现Json解析失败占了绝大多数,并且Json解析失败的城市以及运营商分布和用户反馈非常一致。我们想起在测试另外一个客户端的时候也出现过类似问题,当时是因为运营商非要在网页前面插入一个广告,解决方案是把响应头改成json,因为运营商只劫持html页面。(运营商你丢不丢人!)

立马分析服务器的响应头,确实是json的,运营商作祟的嫌疑又被排除了。但是这么重要的线索到这里又没再继续排查下去了。为什么呢?因为当时客户端日志记录非常不全,只知道解析失败,却不知道用户的请求和得到的响应。(客户端日志非常重要,至少能很大范围上降低排查问题的难度,一定要记录全并且测试充分)

幸运的是周六这天晚上9点半联系到一个在线用户可以稳定重现问题,在这之前联系到的用户要么无法重现,要么不在线。我们希望得到他的请求,看看服务器是不是在某个请求下出现异常。为了让他的请求落到我们固定的一台测试服务器上,打了一个测试包给用户装上,用户装上后非常高兴得告诉我们服务可以用了,谢谢我们帮助他解决问题。但我们知道,我们的希望又破灭了,问题再次无法重现。(其实是因为测试服务器用的域名和正式环境的域名不一致因而没有被劫持)

晚上继续解决了三个服务器Bug,就到了周日了。早上看还是有很多反馈,回过头重新想,也许是因为测试环境问题,也许用正式环境的包用户客户端得到的响应会出现变化?可惜前一天晚上的用户不在线了。这天大家到处骚扰朋友才找到一个朋友可以重现问题,给他一个可以回传请求和响应的正式包装上后,问题能重现!看响应时惊奇得发现用户得到的不是json而是另外一个服务的Html页面,原来用户被劫持了。

因为一些敏感问题这里不方便透露更多信息。如果对上述故事有疑惑可以全选文字看隐藏内容。

这里想分享一些经验。
  • 分析用户反馈需要先明确现象
  • 重现问题时环境要保持一致
  • 客户端异常日志应尽可能记录完整
  • 请用户通过手机浏览器访问可能被劫持的URL可以快速定位是否劫持
  • 反劫持的方法有很多种,请尽情发挥想象~
  • 如果通过设置代理的方式获取请求和响应,可以使用TinyHttpProxy,跨平台的哦
  • 如果一定要给用户新的安装包,请找一台访问速度快的服务器并且提供二维码给用户(当时请两位用户装新包花了各花了2个多小时时间)
其实以上这个Bug我们给很多同学分享过,在团队内部也拿了奖,在这里我再顺便再记录一下当时评奖时的一些感悟。
  • 其实对于比赛来说,很多“过程”是你可以添油加醋或者事后再深入了解的,但是“结果”是你无法改变的。你到底做得如何在于解决的过程中是否把握住了重点,把握有多准就是能力的体现。(某大叔所说的root cause,我很认同)
  • 你能不能跳出自己的这块业务做更大的事不是在于你的技术能力,而是是不是有这个意识,所谓大局观、眼界也是非常重要的能力。当时我觉得我们已经尽可能面面俱到把一切防范措施做好了,但是之后和一个同事聊天的时候却发现其实我们可以做到更多,比如如何从平台的角度帮助更多的系统免于被劫持的风险。
No.372014.03.20周四13:559:00-18:00

如何使用iConsole在客户端上快速查看日志

https://github.com/nicklockwood/iConsole

将iConsle和GTM源码加入工程

让etao4iphoneAppDelegate实现iConsoleDelegate

etao4iphoneAppDelegate.h

实现iConsoleDelegate的handleConsoleCommand方法

etao4iphoneAppDelegate.m

在需要输出日志的地方

参考:http://www.pan-apps.com/585.html

No.382014.03.20周四14:309:00-18:00

Mbox性能优化(一)使用multicurl并发请求减少后端访问时间

今年做了一个平台称为Mbox用于数据效果监控,大概描述下主要的技术方案:client端制定抓取规则和评测规则,server端根据抓取条件请求数据接口加以处理返回给client端。

由于抓取的请求量非常之大(百万级),所以需要对server端做性能优化。server端使用nginx+php-fpm。

请求是根据一定的规则生成的,比如关键词、类目、用户类型、ABTEST等等参数做一个组合。一开始是client端使用多进程的方式调用server端,但是这样会导致server端的cpu很高,并且server端有一个对比评测页面,会同时调用两次server的接口(最早页面是使用异步请求的方式,但由于某些原因改成了同步性能就要求更高了),所以第一步的优化措施是根据某个参数在php并发请求,这里选择的是ABTEST的参数,对比评测页面上操作也是ABTEST之间的对比。

改成并发之后的好处是显而易见的,从对比评测页面请求来说响应时间就省了一半,另外以URL作为key存放响应结果,即使在页面上选择了两个相同的ABTEST对比也不会产生多余的请求。

对于client端,并发操作也更为简单,只需要将多个ABTEST参数值同时传入,不用自己控制并发,维护成本也降低了。

参考文章:http://www.searchtb.com/2010/12/using-multicurl-to-improve-performance.html  感谢玄悲大师。


function multi_curl_fetch($urls, $delay = 1, $timeout = 5){

$queue = curl_multi_init();

$map = array();

foreach ($urls as $module => $url) {

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $url);

curl_setopt($ch, CURLOPT_ENCODING, "gzip");

curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);

curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1);

curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);

curl_multi_add_handle($queue, $ch);

$map[(string) $ch] = $url;

}

$responses = array();

do {

while (($code = curl_multi_exec($queue, $active)) == CURLM_CALL_MULTI_PERFORM) ;

if ($code != CURLM_OK) { break; }

// a request was just completed -- find out which one

while ($done = curl_multi_info_read($queue)) {

// get the info and content returned on the request

$info = curl_getinfo($done['handle']);

$error = curl_error($done['handle']);

$results = Func::callback(curl_multi_getcontent($done['handle']), $delay);

$responses[$map[(string) $done['handle']]] = $results;

// remove the curl handle that just completed

curl_multi_remove_handle($queue, $done['handle']);

curl_close($done['handle']);

}

// Block for data in / output; error handling is done by curl_multi_exec

if ($active > 0) {

curl_multi_select($queue, 0.5);

}

} while ($active);

curl_multi_close($queue);

$res = array();

foreach($urls as $module => $url){

$res[$module] = $responses[$url];

}

return $res;

}

CLOSE ✕