• 软件测试技术
  • 软件测试博客
  • 软件测试视频
  • 开源软件测试技术
  • 软件测试论坛
  • 软件测试沙龙
  • 软件测试资料下载
  • 软件测试杂志
  • 软件测试人才招聘
    暂时没有公告

字号: | 推荐给好友 上一篇 | 下一篇

Linux 笔记本基于“敲打”的命令

发布: 2007-5-26 11:30 | 作者: Nathan Harrington | 来源: IBM | 查看: 1737次 | 进入软件测试论坛讨论

领测软件测试网

有史以来第一次,您可以敲打一下计算机并得到有意义的响应!使用 Linux® 和 Hard Drive Active Protection System(硬盘活动保护系统,HDAPS)内核驱动程序,我们可以访问 Lenovo(以前称为 IBM®)ThinkPads 上的嵌入式加速器,然后处理加速器的数据来读取特定 “敲打” 事件序列(也就是您使用关节敲打笔记本的事件序列),并基于这些敲打事件运行一些命令。双击锁定屏幕,然后敲入密码来解锁。敲打显示屏一次就可以让 MP3 播放器前进一个音轨。这类可能事物是无穷无尽的。

2003 年,IBM 开始发行集成了加速器和相关软件的 ThinkPad 笔记本,以便在笔记本掉到地上时对硬盘进行保护。来自 IBM 和其他地方的黑客已经为 Linux 内核开发了一些模块来利用这些传感器的优点。屏幕显示方向、桌面切换、甚至是游戏控制和实时的笔记本倾斜度 3D 模块现在都已经可以使用了。本文将展示 “敲打代码” 这种新技术和一个简单程序,该程序在检测到特定的敲打代码时会运行一些命令。

使用带有 HDAPS 驱动的已更新的内核,我们就可以用一个简单程序 knockAge 来生成敲打代码了。我们也可以下载并使用一个 Perl 脚本来定制自己的敲打输入环境。请参阅本文最后的 下载 和 参考资料 部分给出的链接,其中包括了解 knockAge 操作的链接。

硬件需求

容易实现
正如您从展示视频(请参见下面 参考资料 中的链接)可以看到的,敲打操作是由一系列指节敲打构成的。尽管 ThinkPad 的加速器是为保护它不受意外事件的影响,但太强力的振荡仍然会对硬盘造成损坏。因此我们必须小心。

很多在 2003 年以及这以后生产的 IBM(现在是 Lenovo)的 ThinkPads 中都有 HDAPS 硬件。如果您不确定自己的硬件配置,可以检查 Lenovo 的 Web 站点上关于您自己型号的机器的技术细节。如果您的机器上没有 ThinkPad,那么这段代码可能无法在您的笔记本上正常工作。

本文是在 x86 体系架构上编写的。本文中的代码是在 ThinkPad T42p 的两个不同模块上进行开发和测试的。有关 ThinkPad 硬件的链接,请参阅 参考资料 部分。

如果您有一台 Apple MacBook,那么您可能也有这种加速器,并且可以使用相同的方法,通过内核访问它们。然而,本文中的代码并没有在 Apple 硬件上进行测试。

软件要求

HDAPS 驱动程序必须包括在内核中才能启用对加速器的访问。试图对现有内核增加补丁也不会获得成功,因此我们建议从自己喜欢的镜像站点上下载最新的内核。新内核发行版中已经包含了对 HDAPS 驱动程序的支持。

启动内核配置选择程序,并在配置中包含 HDAPS 驱动程序。HDAPS 驱动程序位于 Device Drivers > Hardware Monitoring Support > IBM Hard Drive Active Protection System (hdaps) 选项中。更多的内核配置和安装过程已经超出了本文的范围,但是在 Web 站点上有很多教程可以提供具体的帮助;有关可以帮助我们入门的链接,请参阅 参考资料 一节的内容。

本文是在 2.6.15.1 版本的内核上进行开发和测试的。

创建简单的敲打序列

从 下载 一节的链接中下载源代码,并从中找到 knockAge.pl 脚本。这就是让我们可以创建敲打序列的主要 Perl 程序,它还允许监听特殊的敲打序列并运行命令。下面让我们来介绍一下这个用户空间程序的用法,以及 knockAge.pl 程序的配置,然后再对这个函数进行回顾。

使用下面的命令运行 knockAge.pl 程序:

perl knockAge.pl -c

这会启动 Perl 程序来监听敲打事件并记录下它们之间的间距以供将来使用。一旦程序开始运行之后,对笔记本进行的敲打操作就会产生效果。我们并不需要在物理上移动自己的 ThinkPad 来注册敲打事件,如果 ThinkPad 在一个平面上,只要对其进行一些移动和滑行即可。我建议您用左右握住 ThinkPad 左边接近连接轴的地方,同时用右手在距离 LCD 底部 3 英寸的地方敲打显示屏即可。请参阅 下载 部分给出的视频展示,或参阅 参考资料 中用来创建敲打序列的例子。

体验不同的敲打幅度和力度,从而了解 knockAge 程序能够捕获的事件判断率。对于创建复杂的敲打事件来说,这非常重要。

第一次真正尝试敲打应该非常简单,两次双击之间停留 0.5 秒,然后再次运行 perl knockAge.pl -c,在看到 “enter a knock sequence” 时稳定地敲打 LCD 边上两次,中间停留 0.5 秒。在 4 秒之后会自动超时(这是可以配置的),您所敲打的序列会被打印出来,这类似于下面的例子:

0 540031 _#_ (command here) _#_ <comments here>

让我们来分析一下这一行的内容:敲打序列,分隔符,命令区,分隔符,最后是注释区。我们的下一个步骤是将这行内容复制到 knockAge.pl 程序使用的默认配置文件 {$HOME}/.knockFile 中,该配置文件也可能是 /home/<username>/.knockFile 文件。在使用上面的敲打序列行创建好 .knockFile 文件之后,就可以对这行进行修改来运行程序了。将 (command here) 文本修改成 /bin/echo "double tap",并将注释区的内容修改成更有意义的内容,例如:

0 540031 _#_ /bin/echo "double tap" _#_ Double tap event

现在我们已经修改好这个配置文件,可以打印一条通知了,接下来使用下面的命令在守护模式下运行 knockAge 脚本:

perl knockAge.pl

这个程序会在后台安静地监听 ~/.knockFile 所罗列的事件。请使用相同的间隔再次双击屏幕,您会看到在屏幕上打印出了 “double tap” 消息。如果我们希望更详细地了解 knockAge.pl 脚本是如何工作的,那么我们可以使用下面的命令在守护模式下运行它:

perl knockAge.pl -v

使用 xscreensaver 锁定屏幕或打开屏幕

创建 “password” 序列

使用下面的命令在 “create” 模式下运行 knockAge.pl 程序:

perl knockAge.pl -c

现在我们需要创建一个解锁的密码序列;我建议使用 “刮脸和理发的动作”。请确保每次您都可以以一贯精确的方式执行这个动作。尽管您可以通过修改参数来控制输入密码敲打操作所需要的精度,但是这仍然很难匹配精确的时间。“刮脸和理发动作” 除了可以提供稳定的击打顺序之外,其复杂性和简单性对于屏保解锁密码来说也非常适合。下面是一个 “刮脸和理发动作” 的击打序列示例:

0 564025 1185795 621350 516038 960035 444421 _#_ /bin/echo "shave the haircut" _#_ two bits

在进行下一步操作之前,您应该体验一下上面的命令和 ~/.knockFile 配置文件中的双击命令。这可以在屏保运行时提供很好的帮助,它更难检测出敲打是否正确。

xscreensaver 所使用的命令配置

以下设置假设您已经登录到了窗口管理器中,并且已经使用您的 userid 启动了xscreensaver 程序。例如,如果您正在运行 Fedora Core 4,并且使用 gdm 登录到 KDE 中,那么 xscreensaver 就会自动启动。因此,要激活它,则需要将双击命令从:

/bin/echo "double tap"

修改为:

xscreensaver-command -activate &

现在,每次识别出有 “双击” 事件发生时,xscreensaver 程序都会使用所指定的内容来激活。一旦 screensaver 被激活,就可以通过输入密码(如果是这样配置的)对屏幕进行解锁。不过我们真正希望的是自己的朋友也可以使用密码解锁代码来解除屏保。因此,我们需要在 ~/.knockFile 文件中将下面的命令:

/bin/echo "shave the haircut"

替换为:

killall xscreensaver ; nohup xscreensaver -nosplash >/dev/null 2>/dev/null &

这个命令会停止当前运行的所有 xscreensaver 程序,然后在后台再重新启动 xscreensaver。现在我们可以通过敲打屏幕边来重复加锁和解锁计算机屏保的过程。这比蓝牙提供的近似度加锁更加安全或更方便吗?答案可能是否定的。它更酷吗?当然!

更多例子

HDAPS 传感器和 knockAge.pl 程序提供了另外一种用户输入设备,我们可以使用它们以独特的方式进行输入。例如:

  • 如果计划在一个基础上测试新的 X 配置文件,可以将双击条目更改为重新启动配置好的 X 服务器。这样就不需要敲任何其他键来强制重启了。
  • 在命令区中可以放上我们喜欢使用的任何 shell 脚本,这样就可以使用双击来查看 e-mail。
  • 以最新的组合节拍进行敲打,让 ThinkLight 显示 WWII 代在 Kinakuta 的黄金存储设备的 Morse 密码位置。
  • 敲入 Morse 编码,防止键盘输入被记录。

请参阅 参考资料 部分给出的有关将 ThinkPad 的倾斜度用于游戏、显示工具的例子。或者直接跳过这部分内容,将 Threshold 变量设置为 15,这样您使劲踢一脚 ThinkPad,它就会自动重启了。

knockAge.pl 代码

历史和策略

Jeff Molofee 所编写的 hdaps-gl.c 是 knockAge.pl 代码的基础。Hdaps-gl.c 是一个非常好的展示程序,可以展示如何使用倾斜传感器来实时地显示有关 ThinkPad 的方向的信息。二者之间的区别是本例将时间上隔离的事件组织在一起创建了敲打事件,同时提供了相关的代码来创建并监听敲打事件序列。

参数配置

下面让我们来使用对时间和传感器敏感的一些参数来启动 knockAge.pl:


清单 1. 主程序参数

require 'sys/syscall.ph';  # for subsecond timing
            my $option = $ARGV[0] || ""; # simple option handling
            # filename for hdaps sensor reads
            my $hdapsFN = "/sys/devices/platform/hdaps/position";
            my $UPDATE_THRESHOLD =   4;      # threshold of force that indicates a knock
            my $INTERVAL_THRESHOLD = 100000; # microseconds of time required between knock
            # events
            my $SLEEP_INTERVAL =     0.01;   # time to pause between hdaps reads
            my $MAX_TIMEOUT_LENGTH = 4;      # maximum length in seconds of knock pattern
            # length
            my $MAX_KNOCK_DEV =      100000; # maximum acceptable deviation between recorded
            # pattern values and knocking values
            my $LISTEN_TIMEOUT =     2;      # timeout value in seconds between knock
            # events when in listening mode
            

这些变量及其注释都非常简单。它们的用法和配置选项在本文后面部分会进行解释。下面是其余的一些全局变量及其描述。


清单 2. 敲打模式参数

my @baseKnocks = ();             # contains knock intervals currently entered
            my %knockHash = ();              # contains knock patterns, associated commands
            my $prevInterval =       0;      # previous interval of time
            my $knockCount =         0;      # current number of knocks detected
            my $restX = 0; # `resting' positiong of X axis accelerometer
            my $restY = 0; # `resting' positiong of Y axis accelerometer
            my $currX = 0; # current position of X axis accelerometer
            my $currY = 0; # current position of Y axis accelerometer
            my $lastX = 0; # most recent position of X axis accelerometer
            my $lastY = 0; # most recent position of Y axis accelerometer
            my $startTime = 0;  # to manage timeout intervals
            my $currTime  = 0;  # to manage timeout intervals
            my $timeOut   = 0;  # perpetual loop variable
            my $knockAge  = 0;  # count of knocks to cycle time interval
            

子程序

在我们的子程序清单中首先是一个简单的逻辑块,用来检查是否有加速器可读:


清单 3. 检查加速器的子程序

sub checkAccelerometer() {
            my $ret;
            $ret = readPosition ();
            if( $ret ){
            print "no accelerometer data available - tis bork ed\n";
            exit(1);
            }
            }#checkAccelerometer
            

Jeff Molofee 编写的 hdaps-gl.c 代码为 knockAge.pl 中的所有代码提供了一个很好的起点。在下面的 readPosition 子程序中,我们可以看到他的注释。这个子程序将打开一个文件,从中读取当前的加速器数据,然后关闭文件,并返回不包含 “,(逗号)” 字符的数据。


清单 4. readPosition subroutine

## comments from Jeff Molofee in hdaps-gl.c
            #* read_position - read the (x,y) position pair from hdaps.
            #*
            #* We open and close the file on every invocation, which is lame but due to
            #* several features of sysfs files:
            #*
            #*  (a) Sysfs files are seekable.
            #*  (b) Seeking to zero and then rereading does not seem to work.
            ##
            sub readPosition() {
            my ($posX, $posY) = "";
            my $fd = open(FH," $hdapsFN");
            while( <FH> ){
            s/\(//g;
            s/\)//g;
            ($posX, $posY) = split ",";
            }# while read
            close(FH);
            return( $posX, $posY );
            }#readPosition
            

getEpochSecondsgetEpochMicroSeconds 提供了有关敲打模式状态的详细而精确的信息。


清单 5. 时间分隔器

sub getEpochMicroSeconds {
            my $TIMEVAL_T = "LL";      # LL for microseconds
            my $timeVal = pack($TIMEVAL_T, ());
            syscall(&SYS_gettimeofday, $timeVal, 0) != -1 or die "micro seconds: $!";
            my @vals =  unpack( $TIMEVAL_T, $timeVal );
            $timeVal = $vals[0] . $vals[1];
            $timeVal = substr( $timeVal, 6);
            my $padLen =  10 - length($timeVal);
            $timeVal = $timeVal . "0" x $padLen;
            return($timeVal);
            }#getEpochMicroSeconds
            sub getEpochSeconds {
            my $TIMEVAL_T = "LL";      # LL for microseconds
            my $start = pack($TIMEVAL_T, ());
            syscall(&SYS_gettimeofday, $start, 0) != -1 or die "seconds: $!";
            return( (unpack($TIMEVAL_T, $start))[0] );
            }#getEpochSeconds
            

接下来是 knockListen 子程序,前 5 行负责读取当前的加速器数据值,并对基本的值读取进行调整。如果加速器的数量在某一维度上大于更新上限值,那么 checkKnock 变量就被设置为 1。为了调整这个程序,使它只响应我们需要的敲打事件或类似的加速值,我们需要扩大更新上限。例如,我们可以将 ThinkPad 放到自己的汽车中,并让它在检测到硬加速(或减速)时更改 MP3 播放列表。

如果敲打笔记本的力度足够大,并且大于了更新上限,那么就会导致调用 getEpochMicroSeconds 子程序。然后 diffInterval 变量会在两次敲打事件之间被赋值。这个值将很多击打力度大于更新上限的很多快速加速读取压缩到一个时间中。如果没有间隔上限检查,一次硬敲打就会被注册成很多事件,就仿佛是加速器连续一段时间产生大量事件一样。这种行为对于用户的视力和触觉来说都是无法感知到的,但对于 HDAPS 来说显然并非如此。如果已经达到了间隔上限,那么敲打间隔会被记录在 baseKnocks 数组中,然后将两次敲打之间的间隔重置。

仔细修改这些变量可以帮助对程序进行优化,从而识别出您特有的敲打风格。缩小更新上限并扩大周期上限可以检测出更多间隔的轻微敲打。机械敲打设备或特定的敲打方法可能会需要降低间隔上限,从而识别出独特的敲打事件。


清单 6. knockListen 子程序

sub knockListen() {
            my $checkKnock = 0;
            ($currX, $currY) = readPosition();
            $currX -= $restX;  # adjust for rest data state
            $currY -= $restY;  # adjust for rest data state
            # require a high threshold of acceleration to ignore non-events like
            # bashing the enter key or hitting the side with the mouse
            if( abs ($currX) > $UPDATE_THRESHOLD) {
            $checkKnock = 1;
            }
            if( abs ($currY) > $UPDATE_THRESHOLD) {
            $checkKnock = 1;
            }
            if( $checkKnock == 1 ){
            my $currVal = getEpochMicroSeconds();
            my $diffInterval = abs($prevInterval - $currVal);
            # hard knock events can create continuous acceleration across a large time
            # threshold.  requiring an elapsed time between knock events effectively
            # reduces what appear as multiple events according to sleep_interval and
            # update_threshold into a singular event.
            if(  $diffInterval > $INTERVAL_THRESHOLD ){
            if( $knockCount == 0 ){ $diffInterval = 0 }
            if( $option ){
            print "Knock: $knockCount ## last: [$currVal] curr: [$prevInterval] ";
            print "difference is: $diffInterval\n";
            }
            push @baseKnocks, $diffInterval;
            $knockCount++;
            }# if the difference interval is greater than the threshold
            $prevInterval = $currVal;
            }#if checkknock passed
            }#knockListen
            

在创建敲打模式时,该模式会被放入 ~/.knockFile 文件中,并使用下面的子程序进行读取:


清单 7. 读取敲打文件

sub readKnockFile {
            open(KNCKFILE,"$ENV{HOME}/.knockFile") or die "no knock file: $!";
            while(<KNCKFILE>){
            if( !/^#/ ){
            my @arrLine = split "_#_";
            $knockHash{ $arrLine[0] }{ cmd }     = $arrLine[1];
            $knockHash{ $arrLine[0] }{ comment } = $arrLine[2];
            }#if not a comment line
            }#for each line in file
            close(KNCKFILE);
            }#readKnockFile
            

knockListen 获得敲打模式时,它会将该模式与从 readKnockFile 中读取的敲打模式进行比较。下面的 compareKnockSequences 子程序会对敲打之间的时间进行简单的区别检查。注意,敲打之间的差别并不是简单混合在一起的:很多次敲打时的少量时间差别并不会累积成总体的匹配失效。

第一个要比较的是敲打的次数,因为我们没有必要将一个七次敲打的序列与一个两次敲打的序列进行比较。如果敲打的次数与 ~/.knockFile 中现有的敲打序列匹配,每次敲打之间的差别也少于最大敲打偏差,那么这次敲打就可以认为是完全匹配的。在允许敲打序列进行不精确匹配时,最大敲打偏差非常关键。我们可以增大最大敲打偏差来使敲打节奏更加自由,但是要注意,这可能会导致敲打模式匹配不正确。例如,我们可以在所期望的时间之前或之后半秒钟允许自己的敲打模式发生偏离,但这仍然可以匹配。这样就可以有效地说明 “刮脸和理发” 可以与 “Mary 姓 Little Lamb” 匹配,因此在修改这个参数时一定要小心。

如果完整的模式可以匹配,就会运行 ~/.knockFile 中指定的命令,如果启用了冗余模式,则会打印结果。下一个步骤是如果没有找到匹配项,就退出这个子程序;如果找到了匹配项,就重置所记录的敲打序列。这个步骤会执行 compareKnockSequences 子程序:


清单 8. 比较敲打序列

sub compareKnockSequences {
            my $countMatch = 0;  # record how many knocks matched
            # for each knock sequence in the config file
            for( keys %knockHash ){
            # get the timings between knocks
            my @confKnocks = split;
            # if the count of knocks match
            if( $knockCount eq @confKnocks ){
            my $knockDiff = 0;
            my $counter = 0;
            for( $counter=0; $counter<$knockCount; $counter++ ){
            $knockDiff = abs($confKnocks[$counter] - $baseKnocks[$counter]);
            my $knkStr = "k $counter b $baseKnocks[$counter] ".
            "c $confKnocks[$counter] d $knockDiff\n";
            # if it's an exact match, increment the matching counter
            if( $knockDiff < $MAX_KNOCK_DEV ){
            if( $option ){ print "MATCH $knkStr" }
            $countMatch++;
            # if the knocks don't match, move on to the next pattern in the list
            }else{
            if( $option ){ print "DISSONANCE $knkStr" }
            last;
            }# deviation check
            }#for each knock
            }#if number of knocks matches
            # if the count of knocks is an exact match, run the command
            if( $countMatch eq @confKnocks ){
            my $cmd = system( $knockHash{"@confKnocks "}{ cmd } );
            if( $option ){ print "$cmd\n" }
            last;
            # otherwise, make the count of matches zero, in order to not reset
            }else{
            $countMatch = 0;
            }
            }#for keys
            # if the match count is zero, exit and don't reset variables so a longer
            # knock sequence can be entered and checked
            if( $countMatch == 0 ){ return() }
            # if a match occurred, reset the variables so it won't match another pattern
            $knockCount = 0;
            @baseKnocks = ();
            }#compareKnockSequences
            

主程序的逻辑

利用这些子程序,主程序的逻辑允许用户创建敲打序列,或在守护模式下监听敲打序列并执行命令。第一部分是在用户指定 -c 选项(用于创建模式)时执行的。可以用简单的超时进程来结束敲打序列。增大最大超时长度变量的值可以让两次敲打序列之间暂停 4 秒以上。如果我们保留最大超时长度为 4 秒,那么程序运行到这个时间时就会结束,并打印当前输入的敲打序列。


清单 9. 创建序列主逻辑

if( $option eq "-c" ){
            print "create a knock pattern:\n";
            $startTime = getEpochSeconds();  # reset time out start
            while( $timeOut == 0 ){
            $currTime = getEpochSeconds();
            # check if there has not been a knock in a while
            if( $currTime - $startTime > $MAX_TIMEOUT_LENGTH ){
            $timeOut = 1;  # exit the loop
            }else{
            # if a knock has been entered before timeout, reset timers so
            # more knocks can be entered
            if( $knockCount != $knockAge ){
            $startTime = $currTime;   # reset timer for longer delay
            $knockAge = $knockCount;  # synchronize knock counts
            }# if a new knock came in
            }# if timer not reached
            knockListen();
            select(undef, undef, undef, $SLEEP_INTERVAL);
            }#timeOut =0
            if( @baseKnocks ){
            print "place the following line in $ENV{HOME}/.knockFile\n\n";
            for( @baseKnocks ){ print "$_ " }
            print "_#_ (command here) _#_ <comments here>\n\n";
            }#if knocks entered
            

第二部分是用来在一个无限循环中监听敲打序列的主逻辑,它在一个循环中大约要睡眠 1/100 秒。在这个循环中还使用了一个基于秒的超时,在足够的延时之后重置敲打序列。注意,在这个例子中,敲打监听超时时间为 2 秒,而最大超时时间为 4 秒。这样就提供了在敲打创建模式下进行简单测试设置的功能,并为敲打序列的监听模式提供了一个快速重置选项。


清单 10. 主程序中的敲打监听代码

}else{
            # main code loop to listen for knocking and run commands
            readKnockFile();
            $startTime = getEpochSeconds();
            while( $timeOut == 0 ){
            $currTime = getEpochSeconds();
            if( $currTime - $startTime > $LISTEN_TIMEOUT ){
            $knockCount = 0;
            @baseKnocks = ();
            $startTime = $currTime;
            if( $option ){ print "listen timeout - resetting knocks \n" }
            }else{
            if( $knockCount != $knockAge ){
            $startTime = $currTime;   # reset timer for longer delay
            $knockAge = $knockCount;  # synchronize knock counts
            }# if a new knock came in
            compareKnockSequences();
            }#if not reset timeout
            knockListen();
            select(undef, undef, undef, $SLEEP_INTERVAL);
            }#main knock listen loop
            }# if create or listen for knocks
            

警告:安全性

knockAge 程序非常适合用于为系统提供一种额外的用户输入通道。然而,需要注意的是使用 knockAge 来做任何事都需要在系统上进行认证。是的,它可以防止密钥记录程序监听密码的问题,但是很多与 “敲打认证” 有关的因素都表明在对安全性敏感的环境中使用这种技术还不够成熟。敲打序列目前是以 4 到 9 个数字在 ~/.knockFile 中进行存储的,它们以毫秒为单位来表示延时。这个 “密码” 文件非常容易读取,并且通过尝试和匹配敲打模式,可以获得对系统的访问权限。排除毫秒值中一些精度是一种可用的方法,但是这种练习就留给那些希望自行对系统风险进行评估的读者好了。

在任何敏感环境中,我们都应该进行一些研究,判断用户是否有足够的应变能力并能够精确地重现敲打序列。例如,我们是否具有能力创建并连续输入可接受长度的敲打密码?具有普通智商的人是否可以直观地使用这种敲打序列?或者我们是否准备使用 “刮脸和理发操作” 来作为密码?

延伸阅读

文章来源于领测软件测试网 https://www.ltesting.net/


关于领测软件测试网 | 领测软件测试网合作伙伴 | 广告服务 | 投稿指南 | 联系我们 | 网站地图 | 友情链接
版权所有(C) 2003-2010 TestAge(领测软件测试网)|领测国际科技(北京)有限公司|软件测试工程师培训网 All Rights Reserved
北京市海淀区中关村南大街9号北京理工科技大厦1402室 京ICP备2023014753号-2
技术支持和业务联系:info@testage.com.cn 电话:010-51297073

软件测试 | 领测国际ISTQBISTQB官网TMMiTMMi认证国际软件测试工程师认证领测软件测试网