不要追求绝对的公平,红尘之中没有公平而言,人活一世,难得糊涂。                                           it is no use doing what you like, you have got to like what you do.

在RFT中运用手动验证点验证自定义类型对象

上一篇 / 下一篇  2007-01-19 11:59:27 / 个人分类:Rational Functional Tester

8Z1}zRK0Rational Functional Tester(以下简称RFT)是一款强大易用的自动化功能测试工具。在使用RFT进行功能测试的过程中,测试结果的验证往往是通过插入验证点 (Verification Point)来完成的。但是RFT的验证点只能验证有限的数据类型,而在实际应用中,对用户自定义类型的验证存在着较大的需求。本文对验证点的验证执行过 程进行剖析,并介绍如何通过自定义ValueManager来实现对用户自定义类型对象的验证。51Testing软件测试网O eF_*f3q!X ].g%Y

eJG?vM@01验证点简介

~#ArB5_E'K%G"^?051Testing软件测试网[4}{T9{_"S G

1.1 验证点的类型

V)dK2e#`y/r051Testing软件测试网 _`G'xcX Nuep

Rational Functional Tester是Rational最新推出的自动化功能测试工具。RFT具有数据驱动(Data-Driven)测试、scrīptAssure等特性,因 而受到广大功能测试人员的青睐。在RFT中,验证点是脚本(scrīpt)中非常重要的组成部分,它完成对被测试程序生成的实际数据和期望数据的比较,并 将比较结果写入日志。一般情况下,测试的结果是通过对验证点的执行而得到的。51Testing软件测试网CUh N1|4{ q&V:d@4c7a&N
RFT提供了多种形式的验证点,包括:

_6fV` N9T/b?6C5o0Q051Testing软件测试网fO%~Ml/b

静态验证点(Static Verification Point):静态验证点是在录制(Record)RFT脚本的过程中通过向导插入的验证点,它在脚本回放(Playback)的过程中自动被验证。

A!U%| DiC c0

-t CqCgf+T1Y0手动验证点(Manual Verification Point):如果验证点所要验证的内容是由脚本开发人员在脚本中所提供的,则需要建立手动验证点对其进行验证。例如待验证数据来自外部数据源的情况,脚本开发人员需将数据读取后以参数的形式显式传给验证点。51Testing软件测试网I;KKY.?R

w @]b?7tb0动态验证点(Dynamic Verification Point):动态验证点是在脚本首次回放时建立的。验证点一旦建立,其行为就和静态验证点相同了。51Testing软件测试网9?Zi X3W#Q.n1xU

6K8faT2v-L(n0如 果以录制-回放(Record-Playback)模式使用RFT进行图形界面(GUI)的自动化回归测试(Regression Test),较常用的是静态验证点。而由于RFT的数据驱动测试特性以及与其他RUP工具的良好集成,使之也是非图形化界面的功能测试的首选工具之一。在 这些测试用例中,存在着大量的用户自定义类型对象,这些被测试对象并不能在录制过程中被插入对象映射表(ObjectMap)中,也就是不能使用静态验证 点来进行验证,这就需要我们使用手动验证点来比较它们。51Testing软件测试网3vyd\Pv Q

51Testing软件测试网cAP8b(A7Y k+_

1.2 验证点执行过程

,iG0P#K;g!|do _PR051Testing软件测试网0L.YZ \R

在RFT中,手动验证点有两种声明形式:51Testing软件测试网/WSB-@]Ut
? IFtVerificationPoint vpManual (java.lang.String vpName, java.lang.Object actual)51Testing软件测试网!u)lZ"B$|n
该声明接受两个参数,第一个参数为验证点的名称,第二个参数为被测试对象。可以通过如下方式在脚本中调用该方法:51Testing软件测试网$N%}^;f^^+h_ J

L;lQ|{)k0vpManual("VP1", "The object under test").performTest();51Testing软件测试网[ GpX,EQ#I#nR3S

51Testing软件测试网`!YW3~p.M

这条语句的作用就是判断被测试对象和基准线(Baseline)是否一致。这里所说的基准线就是期望数据,它以XML格式被存储在磁盘上,后缀名rftvp。
'[0O/w7t8W8Bmr/@0当回放脚本时,如果基准线所对应的文件已经在磁盘上存在,则RFT会比较被测试对象和基准线中的数据是否一致,如果一致,则测试结果为成功(Pass),否则为失败(Failed);如果基准线尚不存在,则会以当前被测试对象作为基准线存入磁盘。51Testing软件测试网/r_(wx1\9E;D
?? IFtVerificationPoint vpManual (java.lang.String vpName, java.lang.Object expected, java.lang.Object actual)51Testing软件测试网0s4K\`;W^)U

:k+o8p(E8UD0手动验证点的另一种声明接受三个参数,第一个参数为验证点的名称,第二个参数则为期望数据,第三个参数为实际数据,也就是被测试对象。可以通过如下方式在脚本中调用该方法:

,O{-]h nH1x051Testing软件测试网+s5t1{"nOXeH)FU6Ak"V

vpManual("VP1", "Expected object", "The object under test").performTest();

e%k.csgO2a\9o051Testing软件测试网*Bg4ZXGE\%y&W'V

当脚本回放时,RFT直接比较期望数据和实际数据,并将比较的结果被写入日志。总的来说,验证点的执行过程如图1所示:51Testing软件测试网F^Dh H

#d4c2n3H&Y(r;x v051Testing软件测试网vy2xrzT9Z

51Testing软件测试网 _A"@;?v[

图1 验证点执行流程图51Testing软件测试网\/J(Ju)u7G)h}[
51Testing软件测试网lfEU p

51Testing软件测试网![o&H8p8L\a|t

不 难看出,在执行验证点的过程中,涉及到了将数据写入磁盘以及从磁盘中恢复数据的操作。将这些数据写入磁盘便于测试人员使用工具查看和编辑。但是这也带来了 一个问题,对于开发者自定义类型的对象,哪些属性需要被写入磁盘,这些属性按照什么顺序写入都是类型相关的,需要脚本开发者自行定义。更重要的,如何比较 自定义类型也是RFT所不能确定的,换句话说,脚本开发者需要告知RFT对象一致的标准。以上这两个任务都需要通过创建ValueManager来完成。

'N+n.`Vt4\2d&Rn0

:@}{p y0ValueManager 可以用类特定(Class-specific)的形式比较和持久化用户自定义的对象。它是IManageValueClass接口的实现。只有拥有自己的 ValueManager的类型才能作为参数传递给vpManual。这些类型被称为基于Value-class的类型。缺省的,只有基本数据类型, String,Vector等少数类型才是基于Value-class的类型那么,如何才能实现我们自己的ValueManager呢?在下面的章节中, 我们将通过一个实例来帮助您建立自定义类型的ValueManager。

\5k%po&Y|Ue*u0

'~}"g0j0FT | [ v/vC02. 实例分析--如何实现ValueManager51Testing软件测试网{ vm}@7L9|

:@N LX ~ q| N"W02.1需求的提出51Testing软件测试网;}vR0KJ(}3~

&R3rQ/?&c0假设我们需要验证一个计算图形重心的算法。该算法的输出结果为一个自定义类型MyPoint,它表示的一个平面上的点。它具有两个属性x,y,分别表示其横纵座标。MyPoint的实现如下,除了构造函数,它还提供了x和y的getter和setter方法。51Testing软件测试网S.B O1j'g

51Testing软件测试网5R0f)zpF,TJl

51Testing软件测试网XXAM"G9EVG+T
public class MyPoint {
Q#E%J8JB]I/J051Testing软件测试网%dc:EI3o7QP#x
int x;
ikg6]d4g `iU0int y;51Testing软件测试网zfy-n}B G

2t;A!K l j]Na g0public MyPoint(int x, int y) {
(C2T m0O}$p0this.x = x;
OZ0a\:d"_W ~0this.y = y;
LR6v%O;W(wo[0}

$\ zV^"\*r#R7T f051Testing软件测试网.Y'R UD _W$^Yn

public int getX() {51Testing软件测试网*_TaQg9P
return x;51Testing软件测试网mrD_"@%v3u1w
}

A'pEjM051Testing软件测试网0R2FP&p_r/y

public void setX(int x) {
E%GM7OYjL~8C/[d0this.x = x;
r5T;{_YFd3_7]0}

0OvS V)W(E!C'W4~0

]`N"p{]&R0public int getY() {51Testing软件测试网|#y/vxMC$A?
return y;51Testing软件测试网R:lRZ%U\]$R4a S
}

1xe u D B E0

I5Onx2e:v5}^`0public void setY(int y) {51Testing软件测试网6~uSsxg"B.}-E
this.y = y;
-c&iq [:jw$`0}

/xu9z!nB@h&`yS+n0

!Dt,xo(w8nG&[H0}51Testing软件测试网+Z?h] s vhmx

51Testing软件测试网_U\7\%jO3?S0_

我们的测试人员需要判断该算法得到的重心点是否正确,可以使用了如下脚本:

H]/mVW"D'G0

HW9G#D9W#}Q%J4f051Testing软件测试网;`%i/`z-pp
MyPoint point = getCenterOfGravity(polygon);
o7ep(iS(N sz']t0vpManual("VP_CG", point).performTest();

eKYJm(e051Testing软件测试网-B%\n5}'Y%a

上面的语句将在第一次回放脚本时将得到的重心坐标写入磁盘;测试人员可以使用验证点编辑器查看得到的基准线是否正确;在此后的回放中(通常是回归测试中),上述代码的工作就是比较当次回归测试得到的重心对象与基准线是否一致。51Testing软件测试网&\V7|'mB sU

51Testing软件测试网(Q4iT@(~rT

但 是,如果直接调用该语句,RFT就会抛出异常(Unsupported type, value class required),说明MyPoint不是基于Value-class的类型。要使MyPoint成为Value-class,必须为其实现相应的 ValueManager,并部署到RFT中。51Testing软件测试网[H P0X]CN

3lL"?xzF{k!sJ02.2其他验证自定义类型对象的方法51Testing软件测试网0_2_BRC2U

51Testing软件测试网E'[rs)K\;w,?,u

在介绍如何实现ValueManager之前,首先让我们来看一下其他的解决办法。51Testing软件测试网0S^j:M&Xa n J

ycp/B o2`[z0第一种方法,我们可以逐一比较自定义类型的属性来验证被测试对象是否符合要求。如下例所示:51Testing软件测试网.L0W%_7p8ryLQ

D5s:@4X%M_1r5A051Testing软件测试网@5? Y |L&`8Ai
MyPoint point = getCenterOfGravity(polygon);51Testing软件测试网6g;CSb3O[%ay(g
vpManual("VP_CG1", new Integer(point.getX())).performTest();
+J[+HwY/R#TMI~0vpManual("VP_CG2", new Integer(point.getY())).performTest();

7Uh$}(sz r,RR.j,K@0

9iwk Cvk0使用logTestResult方法也可以达到同样的目的:51Testing软件测试网];xz't4eC

Zv+] ^a M0
{GX b/K_\!D;}0MyPoint point = getCenterOfGravity(polygon);51Testing软件测试网(Ur~Y A s$x+D,p(]W
boolean flag = (point.getX() == 6 && point.getY()==8);51Testing软件测试网,{w2d q&l8L P
logTestResult("This is not a VP", flag);

X5c yA4Z'R2o0

0j&W!\`H a0这 两种方式都有其局限性,第一种方式会使验证点的数量增加,特别是当自定义类的属性很多的时候;并且该方法也不利于重用,如果多个脚本都需要验证 MyPoint,则脚本开发的工作会大大增加;第二种方式虽然不增加验证点数目,但是由于logTestResult只记录比较结果,使日志中的信息不 足。特别是在测试用例失败的时候,这不利于测试人员定位问题所在。51Testing软件测试网7~.QR9B"OM.Up
因而,为自定义类型实现ValueManager较以上两种方法更好。

.Oj&TZ c4Hz vt"e051Testing软件测试网O4g.nfUA"Xsy2f

51Testing软件测试网?:V[E"B WE O

51Testing软件测试网6BO)K%Q/K h)s

图3 使用ValueManager则可以通过验证点编辑器查看和编辑验证点数据,利于问题定位
.gvC*m K z0?

%L|7Z&o'gf T051Testing软件测试网BeH} n

图4 使用logTestResult方法只能记录结果,不能记录数据,信息不足51Testing软件测试网0d3Dkn(O?I+R
51Testing软件测试网|NQl7L]/A9PMm/A
2.3创建ValueManager

F.wfh N3Y8|051Testing软件测试网0_AJU%f

现 在,我们准备创建MyPoint的ValueManager--我们命名为MyPointValue了,如上文所述,它是 IManageValueClass接口的一个实现,该接口有7个方法:persistIn, persistInNamed, persistOut, compare, getCanonicalName, getClassName和createValue。这些接口分别完成哪些事情呢?51Testing软件测试网4K*J7JcE6QF

51Testing软件测试网&Dj0j/}-j

我们再来看看,执行验证点的过程中 ValueManager是怎么工作的。如图1所示,验证点执行过程中必须的步骤可以归为三类:读取数据--从基准线中读取数据;写入数据--将期望数据 写入日志,将实际数据写入日志,写入基准线;比较--比较期望数据和实际数据。以上三类动作都需要ValueManager的参与。下面以MyPoint 为例:51Testing软件测试网-lPJU(O ]

51Testing软件测试网0hJ0HeX!p Mq

如图5,在从基准线中读取数据的过程中,如果脚本发现基准线存在,RFT会读取基准线数据。如果RFT在注册了的 ValueManager表中找到了相应的ValueManager--对MyPoint来说,就是找到了MyPointValue--就会创建该 ValueManager的一个实例,然后调用该实例的persistIn方法。persistIn方法包含了如何读取MyPoint内容的逻辑,其返回 值就是一个MyPoint对象,这样就将存储在磁盘上XML格式的MyPoint对象恢复出来,供RFT继续使用了。51Testing软件测试网p2ws#DA#YZZ"g#J g

P}2|8q-i0图5 从基准线中读取数据

(p%KaoNqI#LQ051Testing软件测试网%[O Jgq]u


#[&j/L`CE P0
;S9B|`1H,X#Q0如 图6,在写入数据的过程中,无论是写入期望数据,写入实际数据还是写基准线,都是通过persistOut方法完成的。同样的,RFT首先找到 MyPoint的ValueManger,将其实例化,然后调用MyPointValue的persistOut方法将其写入磁盘。

V%I C\)eQ Z:K1q,S0

aO_!a ?LT/X'r7w g0图6 写入基准线
v?7m9C[S!S7l*@0

#Yr\1q;x9VDi051Testing软件测试网4R*d;WSpO4{ZD

51Testing软件测试网O `Z*_ c'h@4T5Z\
如图7,在比较两个MyPoint对象的时候,RFT还是先找到MyPoint的ValueManger,将其实例化,然后调用MyPointValue的compare方法,得到一个分值,根据分值和用户设置判断期望数据和实际数据是否一致。

5Mi.S Ii0

({~eu;e0图7 比较期望数据与实际数据

7{_*]6XA)[,q.LW r051Testing软件测试网F'Du+v R

51Testing软件测试网1o$Z1}6U!cc&^
理解了IManageValueClass接口各方法的作用,我们就可以来实现这个ValueManager--MyPointValue了。如上文所述,有7个方法需要实现。其中最关键的是4个方法:
9w P4wO MmHU+@0

]i)h$Dp3`/s@{+j051Testing软件测试网gl*vDBFr

持久化输出方法

s SF!l'M0S `051Testing软件测试网4GA0pP/w a

public void persistOut(Object obj, IPersistOut persistout,51Testing软件测试网CU)E ? gI}#j
?IAuxiliaryDataManager auxdatamanager)
w2Vi Z9~051Testing软件测试网"mG$G _-?~h&E

1w4n7k6~R9fW0该方法通过persistout将对象obj的属性写入到磁盘上。obj就是要写入的对象,persistout是负责写操作的接口,由RFT传入,auxdatamanager是用于命名相关文件的接口,由RFT传入,通常不是使用到这个参数。
'vq4D1oG k:BQL0?以MyPoint为例,下面的代码段将MyPoint的属性x,y依次记录到磁盘上:51Testing软件测试网8~EI6K8T/@!c;c8W/H*\ q

,pm#["Rm$~fv0G4i0public void persistOut(Object obj, IPersistOut persistout,
*V k)p*G1c9@(M"[0IAuxiliaryDataManager auxdatamanager) {51Testing软件测试网)mOG4HqM CB
if (obj instanceof MyPoint) {51Testing软件测试网&{#]p g$q9Qc&g2DQ0v
MyPoint point = (MyPoint)obj;

a ](l@p"b6`'HYZ0

$upN*M{P jR0// persistout是负责写的接口,51Testing软件测试网 z6a2['ZN
//write方法接受的第一个参数是要写入的属性的名称
aGT0H:I"o,I0//第二个参数是要写入的属性值
fc tD7rto%K*Ts0persistout.write("X", point.getX());
C{5h0~N-I7Ora0persistout.write("Y", point.getY());51Testing软件测试网y&nNpE2B[
}
_bP+[,a'II Dav;_0}51Testing软件测试网xT/a5_c7a

@t[+wQ wSv J051Testing软件测试网1AMk9Mq1[-n w` Y
对于测试开发人员来说,这里写入了哪些属性,将来哪些属性才能够被恢复出来。
FtQ'{,u9~w0持久化恢复方法
!V/e v(Lvh0RFT 提供两个方法读入持久化了的对象。一种接受IPersistIn类型的参数,另一种接受IPersistInNamed类型的参数。二者的不同在于,前者 是根据对象属性的存储顺序进行读取的,而后者是按照对象的属性名称进行读取。以MyPointValue为例, 方法一的实现如下:

;o/a;W_H]J_r0

%[ _%D:ei SJ._;T~0public Object persistIn(IPersistIn persistin, IAuxiliaryDataManager auxdatamanager) {51Testing软件测试网1v.]5z7zE1X"Oa6w
// 在persistOut方法中,是按照x,y的先后顺序写入磁盘的,
Dk4v|.i&G0// 那么在该方法中就需要按照x,y的顺序将其读取出来read的参数就是写入磁盘的序号。
t{9B"_X H0int x = ((Integer)persistin.read(0)).intValue();
j1P Sow4Q de6Nw0int y = ((Integer)persistin.read(1)).intValue();51Testing软件测试网*U @!e$ys
return new MyPoint(x, y);
1iT,X2wjdZ{0}

`9m_pu0

|"K{2?)YK^-F0方法二的实现如下:

lU"f!U,mo.]$@0

-D-s |GFFw0public Object persistIn(IPersistInNamed persistinnamed, IAuxiliaryDataManager auxdatamanager) {
{RS,fa~P$MR2Z0// 该方法根据persistOut时写入的属性名称读取属性,read的参数是属性名称51Testing软件测试网'ZtHB Jm Y I
int x = ((Integer)persistinnamed.read("X")).intValue();
{l-`IW%@0int y = ((Integer)persistinnamed.read("Y")).intValue();
6C'];JW^jt:n2^t0return new MyPoint(x, y);51Testing软件测试网J @$w0kH2U$b.vqs@{
}51Testing软件测试网6Xi mG6})Y[@*z

51Testing软件测试网2{ l-dLu(g1L

?比较方法

4J&anu#G0

/H5Az"kdA,["t0public int compare(Object obj1, Object obj2, ICompareValueClass comparedvalueclass)51Testing软件测试网BHds"@/]0nOt

51Testing软件测试网CY\w'S`FY(`

compare方法用于确定两个对象一致与否,其前两个参数为要比较的对象,返回值则为一个0-100之间的整数,返回值越大则被比较的对象越相似。
(y!y~zru:S0以MyPointValue为例,其比较原则是,如果二者相等则返回100(表示不同),否则返回0(表示相同)。51Testing软件测试网+KrkS4KU#l

51Testing软件测试网 eqF!KL6` E-u

public int compare(Object obj1, Object obj2, ICompareValueClass comparedvalueclass) {51Testing软件测试网W;ZX `&`RK3d6P8X
if ((obj1 instanceof MyPoint) && (obj2 instanceof MyPoint)) {
@&z NU:v&yzJ5m[0MyPoint point1 = (MyPoint)obj1;
4J'Q9\~ Y&zVf7BF0MyPoint point2 = (MyPoint)obj2;51Testing软件测试网%c u)c E5T#e4`UI
return (point1.getX()==point2.getX() && point1.getY()==point2.getY()) ? 100 : 0;51Testing软件测试网1o s0ti?S
} else {51Testing软件测试网4Q0P6G'ScW
return 0;
Vqme)C7G nc0}51Testing软件测试网S'l on$f K b
}51Testing软件测试网%F4{eRUW%c&O'x k

51Testing软件测试网+G:cp.[y"Zw#S%E

?其他方法51Testing软件测试网*Q }Q/[V\*S6c
除上述四个方法,还有3个方法也需要实现,分别是51Testing软件测试网&@V6l;RyN3G

51Testing软件测试网4Va"H*R [a L

public String getCanonicalName() {51Testing软件测试网{kZLFfNa
return "MyPoint";
P_ a7@:B\,r!] O0}51Testing软件测试网2nV"O1i{ {3Req
51Testing软件测试网X1F)}qI"N

51Testing软件测试网N+i0Hh6T'Gf ~ |

上面的方法返回该Value-class的平台无关的规范名称

d?~gV!Hhj051Testing软件测试网TFF(WZc

public String getClassName() {51Testing软件测试网"L,s2|HD a
return "com.rational.ft.sample.MyPoint";
TWQqw F6Wd0}51Testing软件测试网N7JprY8dc

Vwe2f1ye'A0

~LlK_2{;z0上面的方法返回ValueManager支持的Value-class的名称

:T,UT&O/Y uU0

;`9\KG2q6\9FN0public Object createValue(Object sourceToCopy) {
![[V7k7G$P0return null;51Testing软件测试网9O8P_ pN1xA9^5x|B%f
}51Testing软件测试网;jhg8_} z~a

w9[4I i,JT] S.I0上面的方法返回被测试对象的一个拷贝,可以返回null,较少会被用到。51Testing软件测试网 Z3Z\*hb#m

#cHf's*kg02.4 部署ValueManager

V#WnrrF'}8}051Testing软件测试网p6S\'}y.g9f@

上面的7个方法实现了,MyPoint的ValueManager也就完成了。要在工程中使用ValueManger,还必须将ValueManager部署到RFT中。这也是注册ValueManger的过程。51Testing软件测试网#v^2kV @H w x#P
首先我们需要将ValueManager的实现导出为jar文件,例如vmsample.jar。

Q|h(D/B0

-[ s7NtX0第 二步,我们还需要创建RFT自定义文件。RFT自定义文件是后缀名为RFTCUST的XML文件,用于定义开发者扩展的proxy, valuemanger等。RFT自定义文件内容如下,不难看出,其中ComponentModel元素中Obj一段是用于定义我们所创建的 ValueManger的。ValueClass是要被验证的自定义类型;Manager是该自定义类型的ValueManger。

P%Q1HM2qh051Testing软件测试网S%C^7F0o?


2EH)[2d8u0<ConfigFile L=".ConfigFile">
kR*] Fk/CJ0<Section L=".ConfigFileSection">51Testing软件测试网b!Rc7pH
<Name>valueManagers</Name>
Uz Gc jz p3]0<Val L=".ValueManagerManager">

}OLo$l*J+G ^051Testing软件测试网)X+gdqP,y&M _4\{*v

<ComponentModel L=".ComponentModel">
#},ri"M2V6uL^0<Name>Java</Name>51Testing软件测试网#{F8@!P8}"cD|
<Obj L=".ValueManager">
f3o2r"JWKf}Nl0<Id>.MyPoint</Id>
)?#}7k:]sAo(G0<ValueClass>com.rational.ft.sample.MyPoint</ValueClass>51Testing软件测试网.E-wEL,q
<Manager>com.rational.ft.sample.valuemanager.MyPointValue</Manager>51Testing软件测试网 u9X9d^ym
</Obj>
2Ona}3b5ci-Y0</ComponentModel>
2V6OH"sJ K0</Val>51Testing软件测试网5W6a ^ P-SAS1TQ
</Section>
N,oys X;k X{7t0</ConfigFile>51Testing软件测试网8a(S#IY ~ Q'G euH

51Testing软件测试网s2U9v&xOKz

将. jar文件和RFTCUST建立好后,把这两个文件都放到C:\Documents and Settings\All Users\Application Data\ibm\RFT\customization目录下,ValueManager即被部署到RFT上了。(有可能需要重新启动计算机51Testing软件测试网F2i8Y fY[tN

51Testing软件测试网9T?}4brj Wv

结论
5]h's4m/E0验证点是脚本的重要组成部分。对自定义类型的验证又是测试中所不可避免的。通过开发ValueManager来扩展RFT对自定义类型验证的支持,较之其它方法可重用性好,并使信息能够在日志中一目了然,而且便于修改期望数据。这一特性使RFT的应用更加自由。

-l4[qk V \4XPu,A0

TAG:

 

评分:0

我来说两句

Open Toolbar