最近接触到
web service的一些事情,是基于脚本的自动化的一个
测试过程,主要用到的是
SoapUI(groovy script)。
SoapUI的在线文档是一个很好的资源,基于此,有一些简单的应用分享。
有兴趣可以研究一下,自己也是一个在
学习的过程,一点学习笔记分享给大家。
Here is a sample presented that how to run
test case in soapUI project and save data into DB
Process:
1. Load properties file or DataSource so that fetch corresponding data
2. Set a request with parameters populated(data derive from properties or DataSource)
3. Through transfer property to save data into external files or DB
4. Using DataSink to export data to external files(such as excel,txt) or DB(need to connect DB)
Screenshot of structure of one TestSuite:
Step1: Load files( here I load file from properties file)
PlaceName=Houston
Step2: with data populated request
(get data from properties under Form format)
Step3: transfer data to target
Also can transfer the designated data from original source to target( click on here)
English » | | | | | | | | |
Text-to-speech function is limited to 100 characters
By Design- 就是这么设计的,无效的Bug
Duplicate- 这个问题别人已经发现了,重复的Bug
External- 是个外部因素(比如浏览器、
操作系统、其他第3方软件)造成的问题
Fixed- 问题被修理掉了。Tester要尽可能找到这种Bug
Not Repro- 无法复现你这个问题,无效的Bug
Postponed- 是个问题,但是目前不必修理了,推迟到以后再解
Won't Fix- 是个问题,但是不值得修理了,不管它吧
English » | | | | | | | | |
Text-to-speech function is limited to 100 characters
以下是CakeTestCase类的断言,也就是cakephp 定义的断言,实际使用中还可以使用CakeTestCase的父类 PHPUnit_Framework_TestCase里面的断言
1、assertEqual
2、assertNotEqual
是否不相等
3、assertPattern
是否符合正则匹配
4、assertIdentical
是否恒等(类型一样)
5、assertNotIdentical
是否不恒等
6、assertNoPattern
是否符合正则不匹配
7、expectException
是否会遇到一个异常
8、assertReference
是否会遇到一次跳转
9、assertIsA
是否是对象
10、assertWithinMargin
在一个范围内波动
English » | | | | | | | | |
Text-to-speech function is limited to 100 characters
第一部分
Andoird的SQLiteOpenHelper类中有一个onUpgrade方法。帮助文档中只是说当
数据库升级时该方法被触发。经过实践,解决了我一连串的疑问:
1. 帮助文档里说的“数据库升级”是指什么?
你开发了一个程序,当前是1.0版本。该程序用到了数据库。到1.1版本时,你在数据库的某个表中增加了一个字段。那么软件1.0版本用的数据库在软件1.1版本就要被升级了。
2. 数据库升级应该注意什么?
软件的1.0版本升级到1.1版本时,老的数据不能丢。那么在1.1版本的程序中就要有地方能够检测出来新的软件版本与老的数据库不兼容,并且能够有办法 把1.0软件的数据库升级到1.1软件能够使用的数据库。换句话说,要在1.0软件的数据库的那个表中增加那个字段,并赋予这个字段默认值。
3. 程序如何知道数据库需要升级?
SQLiteOpenHelper类的构造函数有一个参数是int version,它的意思就是指数据库版本号。比如在软件1.0版本中,我们使用SQLiteOpenHelper访问数据库时,该参数为1,那么数据库版本号1就会写在我们的数据库中。
到了1.1版本,我们的数据库需要发生变化,那么我们1.1版本的程序中就要使用一个大于1的整数来构造SQLiteOpenHelper类,用于访问新的数据库,比如2。
当我们的1.1新程序读取1.0版本的老数据库时,就发现老数据库里存储的数据库版本是1,而我们新程序访问它时填的版本号为2,系统就知道数据库需要升级。
4. 何时触发数据库升级?如何升级?
当系统在构造SQLiteOpenHelper类的对象时,如果发现版本号不一样,就会自动调用onUpgrade函数,让你在这里对数据库进行升级。根据上述场景,在这个函数中把老版本数据库的相应表中增加字段,并给每条记录增加默认值即可。
新版本号和老版本号都会作为onUpgrade函数的参数传进来,便于开发者知道数据库应该从哪个版本升级到哪个版本。
升级完成后,数据库会自动存储最新的版本号为当前数据库版本号。
参考:StackOverFlow对“数据库版本在数据库中的存储位置”的问答。
第二部分做Android应用,不可避免的会与SQLite打交道。随着应用的不断升级,原有的数据库结构可能已经不再适应新的功能,这时候,就需要对 SQLite数据库的结构进行升级了。 SQLite提供了ALTER TABLE命令,允许用户重命名或添加新的字段到已有表中,但是不能从表中删除字段。
并且只能在表的末尾添加字段,比如,为 Subscription添加两个字段:
1 ALTER TABLE Subscription ADD COLUMN Activation BLOB;
2 ALTER TABLE Subscription ADD COLUMN Key BLOB;
另外,如果遇到复杂的修改操作,比如在修改的同时,需要进行数据的转移,那么可以采取在一个事务中执行如下语句来实现修改表的需求。
1. 将表名改为临时表
ALTER TABLE Subscription RENAME TO __temp__Subscription;
2. 创建新表
CREATE TABLE Subscription (OrderId VARCHAR(32) PRIMARY KEY ,UserName VARCHAR(32) NOTNULL ,ProductId VARCHAR(16) NOT NULL);
3. 导入数据
INSERT INTO Subscription SELECT OrderId, “”, ProductId FROM __temp__Subscription;
或者
INSERT INTO Subscription() SELECT OrderId, “”, ProductId FROM __temp__Subscription;
* 注意 双引号”” 是用来补充原来不存在的数据的
4. 删除临时表
DROP TABLE __temp__Subscription;
通过以上四个步骤,就可以完成旧数据库结构向新数据库结构的迁移,并且其中还可以保证数据不会应为升级而流失。
当然,如果遇到减少字段的情况,也可以通过创建临时表的方式来实现。
English » | | | | | | | | |
Text-to-speech function is limited to 100 characters
说道抽屉效果在iOS中比较有名的第三方类库就是PPRevealSideViewController。一说到第三方类库就自然而然的想到我们的CocoaPods,今天的博客中用CocoaPods引入PPRevealSideViewController,然后在我们的工程中以代码结合storyboard来做出抽屉效果。
一.在工程中用CocoaPods引入第三方插件PPRevealSideViewController.
(1).在终端中搜索PPRevealSideViewController的版本
(2).在Podfile中添加相应的版本库
(3).之后保存一下Podfile文件,然后执行pod install即可
二、为我们的工程添加pch文件
因为用的是XCode6, 上面默认是没有pch文件的,如果我们想使用pch文件,需要手动添加,添加步骤如下
1.在XCode6中是么有pch文件的,如下图
2.创建pch文件
3.配置pch文件
(1)、找工程的Targets->Build Settings->Apple LLVM 6.0 - Language
(2)在Prefix Header下面的Debug和Release下添加$(SRCROOT)/工程名/pch文件,入下图
English » | | | | | | | | |
Text-to-speech function is limited to 100 characters
问题说明:我正在制作仿酷狗播放器,做到音乐播放的部分时用到CSliderUI控件,后台的音频类回去控制CSliderUI的行为 CSliderUI的行为与酷狗的非常不一样,有几样缺陷:
问题1:仅仅能通过点击CSliderUI的某个位置才干触发valuechanged消息,无法通过滑动滑块去触发,这个bug最严重
问题2:点击CSliderUI的某个位置,当鼠标弹起时滑块才改变位置,而其它软件都是鼠标按下时就改变了
问题3:后台有代码一直调用SetValue函数改变滑块的位置时,会和鼠标土洞滑块冲突,表如今滑块会一直来回跳动
问题4:滑块滑动过程中无法通知主窗口正在改变,这点用在音量改变时,通常我们是一边滑动一边就改变了音量,而不是滑动完毕后再改, 为
此我们加入一个新的消息DUI_MSGTYPE_VALUECHANGED_MOVE,把这个消息的定义放到UIDefine.h文件里
#define DUI_MSGTYPE_VALUECHANGED_MOVE (_T("movevaluechanged"))
同一时候出于效率考虑,要让CSliderUI发出这个消息,应该设置属性sendmove为真,默觉得假
我改动的代码能够通过搜索字符串“2014.7.28 redrain”,来查找,方便大家查看源代码
此次改动不会影响控件原有的属性,我个人水平有限,假设有不论什么问题,能够联系我
//=====================================================================================================
问题的描写叙述结束了,当中的大部分问题都是因为在DoEvent函数中对一些逻辑的推断的不足导致的。
问题1的解决:仅仅能通过点击CSliderUI的某个位置才干触发valuechanged消息,无法通过滑动滑块去触发这是因为原来的UIEVENT_BUTTONUP
消息处理不当造成的,原代码为:
if( event.Type == UIEVENT_BUTTONUP ) { int nValue; if( (m_uButtonState & UISTATE_CAPTURED) != 0 ) { m_uButtonState &= ~UISTATE_CAPTURED; } if( m_bHorizontal ) { if( event.ptMouse.x >= m_rcItem.right - m_szThumb.cx / 2 ) nValue = m_nMax; else if( event.ptMouse.x <= m_rcItem.left + m_szThumb.cx / 2 ) nValue = m_nMin; else nValue = m_nMin + (m_nMax - m_nMin) * (event.ptMouse.x - m_rcItem.left - m_szThumb.cx / 2 ) / (m_rcItem.right - m_rcItem.left - m_szThumb.cx); } else { if( event.ptMouse.y >= m_rcItem.bottom - m_szThumb.cy / 2 ) nValue = m_nMin; else if( event.ptMouse.y <= m_rcItem.top + m_szThumb.cy / 2 ) nValue = m_nMax; else nValue = m_nMin + (m_nMax - m_nMin) * (m_rcItem.bottom - event.ptMouse.y - m_szThumb.cy / 2 ) / (m_rcItem.bottom - m_rcItem.top - m_szThumb.cy); } if(m_nValue !=nValue && nValue>=m_nMin && nValue<=m_nMax) { m_nValue =nValue; m_pManager->SendNotify(this, DUI_MSGTYPE_VALUECHANGED); Invalidate(); } return; } |
在最后的推断出能够看到,仅仅有当m_nValue !=nValue时才会发送DUI_MSGTYPE_VALUECHANGED消息,而我们滑动滑块时,通过上面的代码不难分析出,这两个值一直是相等的,所以导致他没有发出消息,所以我们把这个推断凝视掉即可了
问题2的解决:
点击CSliderUI的某个位置,当鼠标弹起时滑块才改变位置,而其它软件都是鼠标按下时就改变了,这是因为原来的UIEVENT_BUTTONDOWN消息的处理不全面造成的,原代码为:
if( event.Type == UIEVENT_BUTTONDOWN || event.Type == UIEVENT_DBLCLICK ) { if( IsEnabled() ) { RECT rcThumb = GetThumbRect(); if( ::PtInRect(&rcThumb, event.ptMouse) ) { m_uButtonState |= UISTATE_CAPTURED; } } return; } |
能够看到原代码没有做对于控件的外观的不论什么改动,我们把代码改动例如以下:
if( event.Type == UIEVENT_BUTTONDOWN || event.Type == UIEVENT_DBLCLICK ) { if( IsEnabled() ) {//2014.7.28 redrain 凝视掉原来的代码,加上这些代码后能够让Slider不是在鼠标弹起时才改变滑块的位置 m_uButtonState |= UISTATE_CAPTURED; int nValue; if( m_bHorizontal ) { if( event.ptMouse.x >= m_rcItem.right - m_szThumb.cx / 2 ) nValue = m_nMax; else if( event.ptMouse.x <= m_rcItem.left + m_szThumb.cx / 2 ) nValue = m_nMin; else nValue = m_nMin + (m_nMax - m_nMin) * (event.ptMouse.x - m_rcItem.left - m_szThumb.cx / 2 ) / (m_rcItem.right - m_rcItem.left - m_szThumb.cx); } else { if( event.ptMouse.y >= m_rcItem.bottom - m_szThumb.cy / 2 ) nValue = m_nMin; else if( event.ptMouse.y <= m_rcItem.top + m_szThumb.cy / 2 ) nValue = m_nMax; else nValue = m_nMin + (m_nMax - m_nMin) * (m_rcItem.bottom - event.ptMouse.y - m_szThumb.cy / 2 ) / (m_rcItem.bottom - m_rcItem.top - m_szThumb.cy); } if(m_nValue !=nValue && nValue>=m_nMin && nValue<=m_nMax) { m_nValue =nValue; Invalidate(); } } return; } |
问题3的解决:
后台有代码一直调用SetValue函数改变滑块的位置时,会和鼠标土洞滑块冲突,表如今滑块会一直来回跳动,这是由于,当我们拖动滑块时会动态的改动Slider的m_nValue值,而且会刷新控件,而通过读PaintStatusImage函数可知,控件正式通过这个m_nValue变量来决定滑块的绘制位置。而我在后台让音乐播放类去依据音乐的进度调用SetValue函数,这个函数理所当然的改动了m_nValue值,这就导致了冲突,这个函数是父类的,所以我们要重写这个函数。当鼠标正在滑动式不让SetValue去改变控件的滑块的位置。
添加SetValue函数然后重写他,改动的代码为:
void CSliderUI::SetValue(int nValue)
{
if( (m_uButtonState & UISTATE_CAPTURED) != 0 )
return;
CProgressUI::SetValue(nValue);
}
问题4的解决:
滑块滑动过程中无法通知主窗口正在改变,这点用在音量改变时,通常我们是一边滑动一边就改变了音量,而不是滑动完毕后再改变, 为此我们加入一个新的消息DUI_MSGTYPE_VALUECHANGED_MOVE,把这个消息的定义放到UIDefine.h文件里,这个代码仅仅要在DoEvent的UIEVENT_MOUSEMOVE消息处理中把DUI_MSGTYPE_VALUECHANGED_MOVE事件传送出去就好了,后来听从网友“不乖打Pp.”的建议,添加了一个属性"sendmove",当属行为真时才发送消息出去。
改动代码为:
if( event.Type == UIEVENT_MOUSEMOVE ) { if( (m_uButtonState & UISTATE_CAPTURED) != 0 ) { if( m_bHorizontal ) { if( event.ptMouse.x >= m_rcItem.right - m_szThumb.cx / 2 ) m_nValue = m_nMax; else if( event.ptMouse.x <= m_rcItem.left + m_szThumb.cx / 2 ) m_nValue = m_nMin; else m_nValue = m_nMin + (m_nMax - m_nMin) * (event.ptMouse.x - m_rcItem.left - m_szThumb.cx / 2 ) / (m_rcItem.right - m_rcItem.left - m_szThumb.cx); } else { if( event.ptMouse.y >= m_rcItem.bottom - m_szThumb.cy / 2 ) m_nValue = m_nMin; else if( event.ptMouse.y <= m_rcItem.top + m_szThumb.cy / 2 ) m_nValue = m_nMax; else m_nValue = m_nMin + (m_nMax - m_nMin) * (m_rcItem.bottom - event.ptMouse.y - m_szThumb.cy / 2 ) / (m_rcItem.bottom - m_rcItem.top - m_szThumb.cy); } if (m_bSendMove) m_pManager->SendNotify(this, DUI_MSGTYPE_VALUECHANGED_MOVE); Invalidate(); } // Generate the appropriate mouse messages POINT pt = event.ptMouse; RECT rcThumb = GetThumbRect(); if( IsEnabled() && ::PtInRect(&rcThumb, event.ptMouse) ) { m_uButtonState |= UISTATE_HOT; Invalidate(); }else { m_uButtonState &= ~UISTATE_HOT; Invalidate(); } return; } |
至此我们就改动了四处地方,还有其它改动的地方大家能够自己看源文件,此次改动不会影响控件原有的属性,我个人水平有限,假设有不论什么问题,能够联系我。
English » | | | | | | | | |
Text-to-speech function is limited to 100 characters
一、static定义
static是静态修饰符意思,什么叫静态修饰符呢?大家都知道,在程序中任何变量或者代码都是在编译时由系统自动分配内存来存储的,而所谓静态就是指在编译后所分配的内存会一直存在,直到程序退出内存才会释放这个空间,也就是只要程序在运行,那么这块内存就会一直存在。这样做有什么意义呢?
在Java程序里面,万物皆对象,而对象的抽象就是类,对于一个类而言,如果要使用他的成员,那么普通情况下必须先实例化对象后,通过对象的引用才能够访问这些成员,但是有种情况例外,就是该成员是用static声明的。
static声明的静态变量可以直接通过类名调用!
1 class Demo{ 2 public static void main(String[] args) 3 { 4 Person p = new Person(); 5 System.out.println(p.country); //1、新建对象调用 6 System.out.println(Person.country); //2、通过类名直接调用 7 } 8 } 9 class Person{ 10 static String country = "china"; 11 } |
二、static特点
由此我们得出static的特点。
a、static是一个修饰符,用于修饰成员。
b、static修饰的成员被所有的对象共享。
c、static优先于对象存在,static成员随着类的加载就已经存在。
d、static修饰成员多一种调用方式--通过类名调用。
三、成员变量和静态变量区别?
1.两个变量的生命周期同
成员变量随对象创建存在,随对象回收而释放。
静态变量随类的加载而存在,同样也随着类而消失。
2、调用方式
成员变量只能被对象调用。
静态变量能被对象调用,还可以被类名调用。
3、内存中存储位置不同。
成员变量存储在堆内存中。
静态变量存储在方法区(共享数据区)的静态区。
四、静态使用注意事项
1、静态方法只能访问静态成员。
2、静态方法中不可以用this或super关键字。
3、主函数都是静态的。
class Demo{ public static void main(String[] args) { Person.show(); } } class Person{ static String country = "china"; String name = "jinfulin"; public static void show() { System.out.print(country); //正确 //System.out.print(name); //错误 } } |
English » | | | | | | | | |
Text-to-speech function is limited to 100 characters
摘要: 单元测试基础 当今软件测试十分盛行时,本人通过项目实践和个人亲身体会浅谈单元测试,本人一直坚持“用代码说话的原则”,同时也希望个人能给出宝贵意见,共同探讨、共同进步,为中国软件事业有更大的发展共同奋斗! 最早我们项目组开发的项目时,写代码都是从底层一直写到表现层到jsp,然后开发人员在web层调试页面,近乎98%都会报一大堆exception,然后再在代码中加断点一步一...
阅读全文
在21世纪做生意,自动化就是问题的实质!当然,
Web应用给企业带来了获得在全球范围数以百万计潜在客户的灵活性,但安全问题的威胁却日益严重。
据Acutenix,Web应用安全行业的领导者,最近的独立分析指出,所有的网络攻击中75%是在Web应用程序level进行的。此外,该公司已经表明,至少有70%的网站是在可能被黑客直接攻击的风险之下!随着越来越多重要和敏感的数据被存储在Web应用程序中,数据相关的处理也相应增加,网络应用程序的
安全测试已经变得至关重要。
安全性测试是为了确保一个web应用程序有足够的能力,防止未经授权的用户访问资源和数据而执行的。在Web应用程序和其他客户端服务器应用程序,安全性测试起着至关重要的作用,因为它可以帮助你在正在进行中的网站或Web应用程序中找出漏洞或弱点。
然而,在你开始Web应用程序安全性测试之前,有几个很重要的你需要知道的有关安全测试中使用的术语。 这里是一些会经常会在Web应用程序测试中使用到的常用术语:
“漏洞”——这不过是一些Web应用程序中的弱点。其背后的主要原因可能是在应用程序中的bug。
“URL处理”——很多web应用程序通过URL来实现客户端和服务器之间的交互或者共享信息。修改URL中的某些信息可能会导致服务器不确定行为。
“
SQL注入”——这不过是通过Web应用程序用户界面向服务器执行的查询中插入SQL语句的过程。
“XSS(跨站脚本)”——每当用户在web应用程序的用户界面插入HTML或其他任何客户端脚本,并且当其被其他人可见时,这被称为跨站脚本!
“欺骗”——这个意味着创造外观类似的网站或电子邮件的骗局。
一旦你熟悉了所有的名词,下一步是开始了解安全测试的不同属性。在执行安全测试的网站或Web应用程序中,有七个基本属性,涵盖了包括认证,授权,保密性,可用性,完整性,不可否认性和韧性。让我们详细了解一下它们:
认证:这不过是一个访问系统前确认访问者身份的过程。只有在成功破解验证过程的情况下才允许用户访问网站或Web应用程序。
授权:一旦用户通过认证,授权就可以用来限制用户访问基于其身份的某些功能。
保密性:这基本上是用来验证是否任何未经授权的用户和无此权限的用户都不能够访问该信息。它有助于保护来自用户的信息和资源授权。
可用性:它将检查系统是否在除了维护安全补丁和升级之外可以让授权用户随时使用。此外,系统的停机时间应尽可能短,来使系统有更高的可用性。
完整性:它保证了信息接收的传输过程中不被修改,并确认是否给不同组的用户都提供了正确的信息。
不可否认性:它追踪谁正在访问系统,那些请求被拒绝,以及其他类似时间戳,IP地址等等的细节。
韧性:它会检查系统是否有足够承受攻击的能力。这可以通过使用加密来实现。
这些都是Web应用程序安全测试的主要属性。然而,这个列表并不详尽。好了,那我们可以在安全测试执行哪些测试? 这里有一些安全测试的主要类型:
安全审计:它主要包括开发应用程序的直接检验。它还包括代码演练。
安全扫描:它涉及到扫描Web应用程序或系统和验证。在这种测试中,审计人员主要检查并找出应用程序中的缺陷。
漏洞扫描:将测试该应用程序的所有可能的漏洞。大多数时候它是利用扫描软件或应用程序来扫描漏洞。
风险评估:这是一种涉及基于损失的类型和损失发生可能性的分析和决定风险的方法。
状态评估及安全性测试:它包括了为了达到安全目的的安全检测,风险评估和道德黑客攻击的组合。
渗透测试:在该方法中,测试人员强行访问和输入被测试应用。测试人员将尝试使用一些其他的应用程序或一些组合应用程序的漏洞,来访问一个网站或系统。
道德黑客:这都是在一个网站或web应用程序的安全性测试中强行入侵外部元素。它也包括了一系列的渗透测试。
请确保您了解并记住所有在这里的介绍,来使您的Web应用程序安全测试有效且成功。希望这些信息可以帮助你成功地执行安全测试。
作者简介: Prashant Chambakara是测试自动化专家。他目前正在与TestingWhiz(为测试Web应用程序自动化的工具)合作。 Prashant喜欢参加并通过博客,
文章和会议演讲活动来对测试社区做出贡献。他的Twitter名是@prashant_geek。
English » | | | | | | | | |
Text-to-speech function is limited to 100 characters
1.测试的前期准备阶段
a.系统基础功能验证,该活动主要确保当前需要进行性能测试的应用已经具备了进行测试的条件
b.组建测试团队
c.测试工具需求确认
2.测试工具引入阶段
a.选择工具
b.工具应用的技能培训
c.确定工具的应用过程
3.测试计划阶段
a.性能测试领域分析
b.用户活动剖析与业务建模
用户活动剖析与业务建模活动用来寻找用户的关键性能关注点。用户对系统性能的关注往往集中在少数几个业务活动上,在确定性能目标之前,需要先把用户的关注点找出来,从而确定最贴近用户要求的性能目标。
用户活动剖析的方法大体分为两种:系统日志分析和用户调查分析。系统日志分析是指通过应用系统的日志了解用户的活动,分析出用户最关注、最常用的业务功能的操作路径;用户调查分析是在不具备系统日志分析的条件(如该系统尚未交付用户运行实际的业务)时采用的一种估算方法,可以通过用户调查问卷、同类型系统对比的方法获取用户最关注、最常用的业务功能等内容。
c.确定性能目标
性能测试目标根据性能测试需求和用户活动分析结果来确定,确定性能测试目标的一般步骤是首先从需求和设计中分析出性能测试需求,结合用户活动剖析与业务建模的结果,最终确定性能测试的目标
d.制定测试时间计划
e.测试设计与开发阶段
1>.测试环境设计
对于能力验证领域的性能测试,首先明确是在特定的部署环境上进行,因此不需要特别为性能测试设计环境,只需要保证用于测试的环境与今后系统运行的环境一致即可。
对于规划能力领域的性能测试,测试环境不特定,但也需要设计一个基准的环境。
对于性能调优领域的性能测试,因为调优过程是一个反复的过程,在每个调优小阶段的末尾,都需要有性能测试来衡量调优的效果,因此必须在开始就给出一个衡量的环境标准,并在整个调优过程中保证每次测试时的环境保持不变。
这里所说的测试环境包括:系统的软硬件环境+数据环境设计+环境的维护
2>.测试场景设计
测试场景模拟的一般是实际业务运行的剖面
3>.测试用例设计
4.测试执行与管理
5.测试分析
English » | | | | | | | | |
Text-to-speech function is limited to 100 characters